What is the Solana pattern for creating a transfer account the program can use to execute a contract payment?

Viewed 2585

I'm attempting to process a transaction in my solana contract. The way it seems I should do this is by using createAccountWithSeed to generate a transfer account owned by both the program (8DqELvN5TFeMtNJciUYvGqso2CyG5M6XNWxh3HRr3Vjv) and payer. So I create the new transfer account to send through to the solana program processor to execute the transaction. But when I pass the transfer account through to my Rust program the check_account_owner states that the account is owned by the System Program (11111111111111111111111111111111) rather than my program.

So my problem is two-fold:

  • Is that the correct pattern to use in this instance?
  • If yes, how do I create an account that both the program and payer can execute?

Here's the JS for the createAccountWithSeed in the client.

const transferAcc = await PublicKey.createWithSeed(
    payer.publicKey,
    "payer",
    PROGRAM_ID,
  );
  await connection.requestAirdrop(transferAcc, 100000);
  SystemProgram.createAccountWithSeed({
    basePubkey: payer.publicKey,
    fromPubkey: payer.publicKey,
    lamports: 100000,
    newAccountPubkey: transferAcc,
    programId: PROGRAM_ID,
    seed: "payer",
    space: 1024,
  });

  const accInfo = await connection.getAccountInfo(transferAcc);
  console.log(
    `Paying from acc: ${transferAcc.toBase58()}, Owned by: ${accInfo?.owner.toBase58()}`
  );

And here's the Rust code which is attempting to make the transfer.

pub fn process_payment(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        payment_fee: u64,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let token_program = next_account_info(account_info_iter)?;
        let payer_acc = next_account_info(account_info_iter)?;
        let transfer_acc = next_account_info(account_info_iter)?;
        let receiver_acc = next_account_info(account_info_iter)?;

        if !payer_acc.is_signer {
            return Err(ProgramError::MissingRequiredSignature);
        }

        if *token_program.key != id() {
            return Err(SosolError::IncorrectTokenProgramId.into());
        }

        check_account_owner(payer_payment_acc, &program_id)?;

        msg!("Calling the token program to transfer tokens to the receiver...");
        token_transfer(
            token_program.clone(),
            transfer_acc.clone(),
            receiver_account_key.clone(),
            payer_acc.clone(),
            payment_fee,
        )?;

        Ok(())
    }

/// Issue a spl_token `Transfer` instruction.
#[allow(clippy::too_many_arguments)]
fn token_transfer<'a>(
    token_program: AccountInfo<'a>,
    source: AccountInfo<'a>,
    destination: AccountInfo<'a>,
    authority: AccountInfo<'a>,
    amount: u64,
) -> Result<(), ProgramError> {
    let ix = transfer(
        token_program.key,
        source.key,
        destination.key,
        authority.key,
        &[],
        amount,
    )?;
    invoke(&ix, &[source, destination, authority, token_program])
}

The error logs state:

    Program log: Expected account to be owned by program 8DqELvN5TFeMtNJciUYvGqso2CyG5M6XNWxh3HRr3Vjv, received 11111111111111111111111111111111
    Program log: CUSTOM-ERROR: The account did not have the expected program id
1 Answers

OK, so the reason why the transfer account is owned by the system program rather than the my program is because I was creating the account outside of the transaction. The key is to add the createAccountWithSeed (or actually just createAccount for me as I actually want a fresh account for each transaction) method into your transaction chain like so:

  const transaction = new Transaction();
  const transferAcc = new Keypair();
  const transferAccPubKey = transferAcc.publicKey;

  transaction.add(
    SystemProgram.createAccount({
      fromPubkey: payerAccount.publicKey,
      newAccountPubkey: transferAccPubKey,
      lamports: paymentFee,
      space: dataLayout.span,
      programId: PROGRAM_ID,
    })
  );

The runloop is a really good partner resource for assistance with this. Once you've added all the transaction items into the transaction item you'll be sending it using:

  return await sendAndConfirmTransaction(connection, transaction, [
    payerAccount, transferAcc
  ]);

So look for that if you're struggling where to insert the transaction.add method.

It took me ages to figure this so hope it helps someone.

Related