rustanchorsolana

Better way to send Solana to multiple public keys in program account using anchor than the way I'm current achieving it?


New Solana Anchor developer here and I have a question about how I am currently sending Sol to multiple wallets in my program. Currently im using the code below twice and then invoking two separate transactions.

system_instruction::transfer(from_account.key, to_account.key, amount);

However, I noticed in the Rust documentation there was a system instruction for "transfer_many" using a vec array of instruction.

Can someone explain to me how I would create 1 instruction with transfer_many and than invoke that instruction? The documentation is gibberish to me so if someone would explain in human terms I would appreciate it.

Here is my full code below to send sol to multiple addresses. Please ignore the "feeaccount" as eventually I will change up my code to send a percent of the sol that the program account accepts. (Later down the road once I learn more)

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer as SplTransfer};
use solana_program::system_instruction;

declare_id!("HBj3u9jbTigqXb47TAF2H44TAoTckVJnjzFc4jxusUN2");

#[derive(Accounts)]
pub struct TransferLamports<'info> {
    #[account(mut)]
    pub from: Signer<'info>,
    #[account(mut)]
    pub to: AccountInfo<'info>,
    #[account(mut)]
    pub feeto: AccountInfo<'info>,
    pub system_program: Program<'info, System>,
}

#[program]
pub mod solana_lamport_transfer {
    use super::*;
    pub fn transfer_lamports(ctx: Context<TransferLamports>, amount: u64) -> Result<()> {
        let from_account = &ctx.accounts.from;
        let to_account = &ctx.accounts.to;
        let fee_account = &ctx.accounts.feeto;

        // Create the transfer instruction
        let transfer_instruction1 =
            system_instruction::transfer(from_account.key, to_account.key, amount);
        // Create the transfer instruction
        let transfer_instruction2 =
            system_instruction::transfer(from_account.key, fee_account.key, amount);

        // Invoke the transfer instruction
        anchor_lang::solana_program::program::invoke_signed(
            &transfer_instruction1,
            &[
                from_account.to_account_info(),
                to_account.clone(),
                ctx.accounts.system_program.to_account_info(),
            ],
            &[],
        )?;

        // Invoke the transfer instruction
        anchor_lang::solana_program::program::invoke_signed(
            &transfer_instruction2,
            &[
                from_account.to_account_info(),
                fee_account.clone(),
                ctx.accounts.system_program.to_account_info(),
            ],
            &[],
        )?;

        Ok(())
    }
}

My code works how I wrote it, however, I'm so lost in using the "transfer_many" snip. Is the code I wrote fine? Or will I run into issues? I noticed that I dont pay a tx fee twice since its in the same call so maybe I dont need to use transfer_many?


Solution

  • TLDR; your code is fine.

    Transferring Sol through a on-chain program requires a cross-program invocation to the System program. This native program has a defined set of possible instructions that you can give it, which are defined in the documentation here as an enumeration. The first thing that we notice is that of the 13 possible instructions, only 2 mention transfer: SystemInstruction::Transfer and SystemInstruction::TransferWithSeed. There are no TransferMany instructions available.

    When we dig a bit deeper into the solana_program::system_instruction::transfer_many() function, we see it returns a Vec<Instruction> where as the transfer() function only returns a single Instruction. If you can find its definition in the source code, you can see the following:

    pub fn transfer_many(from_pubkey: &Pubkey, to_lamports: &[(Pubkey, u64)]) -> Vec<Instruction> {
        to_lamports
            .iter()
            .map(|(to_pubkey, lamports)| transfer(from_pubkey, to_pubkey, *lamports))
            .collect()
    }
    

    This function actually does very little. Rather than taking from_pubkey, to_pubkey and lamports as arguments (such as the transfer() function), the transfer_many() takes a from_pubkey and slice of tuples, each containing a to_pubkey and lamports combination. For each tuple, it calls the regular transfer() method for constructing the Instruction and then returns them all together (as a Vec of Instructions).

    Now, when you are creating a on-chain program, the transfer_many() adds little value, because a cross-program invocation only allows you to specify one instruction. This is different from when creating a client program (e.g. off-chain software that interacts with Solana) where instructions are bundled into a transaction. In such a case it would make sense to use the transfer_many() because you could bundle the set of instructions into one transaction.

    Back in the day, the solana-sdk (for client or off-chain development) and solana_program where combined crates, so that is why you see some of these 'legacy' functions that make little sense when creating a on-chain program but are very logical to have available when building some client application.

    This is actually illustrated by the examples in the aforementioned documentation. The client example constructs multiple transfer instructions using the transfer_many() and directly puts the resulting Vec<Instruction> into a new Transaction. The program example on the same page does a bad job explaining this concept in the context of on-chain development, but the key thing to see is the for-loop at the end:

    for instr in instrs {
            invoke_signed(&instr, accounts, &[&[b"bank", &[bank_pda_bump_seed]]])?;
        }
    

    It actually calls invoke_signed separately for each Instruction from transfer_many().

    Hope this clarifies some bits and pieces for you!