Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 553
harkl Avatar asked Aug 04 '21 03:08

harkl


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.

like image 198
harkl Avatar answered Sep 26 '22 16:09

harkl