solana_tokens/
spl_token.rs

1use {
2    crate::{
3        args::{DistributeTokensArgs, SplTokenArgs},
4        commands::{get_fee_estimate_for_messages, Error, FundingSource, TypedAllocation},
5    },
6    console::style,
7    solana_account_decoder::parse_token::{real_number_string, real_number_string_trimmed},
8    solana_cli_output::display::build_balance_message,
9    solana_instruction::Instruction,
10    solana_message::Message,
11    solana_program_pack::Pack,
12    solana_rpc_client::rpc_client::RpcClient,
13    spl_associated_token_account_interface::{
14        address::get_associated_token_address, instruction::create_associated_token_account,
15    },
16    spl_token_interface::state::{Account as SplTokenAccount, Mint},
17};
18
19pub fn update_token_args(client: &RpcClient, args: &mut Option<SplTokenArgs>) -> Result<(), Error> {
20    if let Some(spl_token_args) = args {
21        let sender_account = client
22            .get_account(&spl_token_args.token_account_address)
23            .unwrap_or_default();
24        spl_token_args.mint = SplTokenAccount::unpack(&sender_account.data)?.mint;
25        update_decimals(client, args)?;
26    }
27    Ok(())
28}
29
30pub fn update_decimals(client: &RpcClient, args: &mut Option<SplTokenArgs>) -> Result<(), Error> {
31    if let Some(spl_token_args) = args {
32        let mint_account = client.get_account(&spl_token_args.mint).unwrap_or_default();
33        let mint = Mint::unpack(&mint_account.data)?;
34        spl_token_args.decimals = mint.decimals;
35    }
36    Ok(())
37}
38
39pub(crate) fn build_spl_token_instructions(
40    allocation: &TypedAllocation,
41    args: &DistributeTokensArgs,
42    do_create_associated_token_account: bool,
43) -> Vec<Instruction> {
44    let spl_token_args = args
45        .spl_token_args
46        .as_ref()
47        .expect("spl_token_args must be some");
48    let wallet_address = allocation.recipient;
49    let associated_token_address =
50        get_associated_token_address(&wallet_address, &spl_token_args.mint);
51    let mut instructions = vec![];
52    if do_create_associated_token_account {
53        instructions.push(create_associated_token_account(
54            &args.fee_payer.pubkey(),
55            &wallet_address,
56            &spl_token_args.mint,
57            &spl_token_interface::id(),
58        ));
59    }
60    instructions.push(
61        spl_token_interface::instruction::transfer_checked(
62            &spl_token_interface::id(),
63            &spl_token_args.token_account_address,
64            &spl_token_args.mint,
65            &associated_token_address,
66            &args.sender_keypair.pubkey(),
67            &[],
68            allocation.amount,
69            spl_token_args.decimals,
70        )
71        .unwrap(),
72    );
73    instructions
74}
75
76pub(crate) fn check_spl_token_balances(
77    messages: &[Message],
78    allocations: &[TypedAllocation],
79    client: &RpcClient,
80    args: &DistributeTokensArgs,
81    created_accounts: u64,
82) -> Result<(), Error> {
83    let spl_token_args = args
84        .spl_token_args
85        .as_ref()
86        .expect("spl_token_args must be some");
87    let allocation_amount: u64 = allocations.iter().map(|x| x.amount).sum();
88    let fees = get_fee_estimate_for_messages(messages, client)?;
89
90    let token_account_rent_exempt_balance =
91        client.get_minimum_balance_for_rent_exemption(SplTokenAccount::LEN)?;
92    let account_creation_amount = created_accounts * token_account_rent_exempt_balance;
93    let fee_payer_balance = client.get_balance(&args.fee_payer.pubkey())?;
94    if fee_payer_balance < fees + account_creation_amount {
95        return Err(Error::InsufficientFunds(
96            vec![FundingSource::FeePayer].into(),
97            build_balance_message(fees + account_creation_amount, false, false).to_string(),
98        ));
99    }
100    let source_token_account = client
101        .get_account(&spl_token_args.token_account_address)
102        .unwrap_or_default();
103    let source_token = SplTokenAccount::unpack(&source_token_account.data)?;
104    if source_token.amount < allocation_amount {
105        return Err(Error::InsufficientFunds(
106            vec![FundingSource::SplTokenAccount].into(),
107            real_number_string_trimmed(allocation_amount, spl_token_args.decimals),
108        ));
109    }
110    Ok(())
111}
112
113pub(crate) fn print_token_balances(
114    client: &RpcClient,
115    allocation: &TypedAllocation,
116    spl_token_args: &SplTokenArgs,
117) -> Result<(), Error> {
118    let address = allocation.recipient;
119    let expected = allocation.amount;
120    let associated_token_address = get_associated_token_address(&address, &spl_token_args.mint);
121    let recipient_account = client
122        .get_account(&associated_token_address)
123        .unwrap_or_default();
124    let (actual, difference) = if let Ok(recipient_token) =
125        SplTokenAccount::unpack(&recipient_account.data)
126    {
127        let actual_ui_amount = real_number_string(recipient_token.amount, spl_token_args.decimals);
128        let delta_string =
129            real_number_string(recipient_token.amount - expected, spl_token_args.decimals);
130        (
131            style(format!("{actual_ui_amount:>24}")),
132            format!("{delta_string:>24}"),
133        )
134    } else {
135        (
136            style("Associated token account not yet created".to_string()).yellow(),
137            "".to_string(),
138        )
139    };
140    println!(
141        "{:<44}  {:>24}  {:>24}  {:>24}",
142        allocation.recipient,
143        real_number_string(expected, spl_token_args.decimals),
144        actual,
145        difference,
146    );
147    Ok(())
148}
149
150#[cfg(test)]
151mod tests {
152    // The following unit tests were written for v1.4 using the ProgramTest framework, passing its
153    // BanksClient into the `solana-tokens` methods. With the revert to RpcClient in this module
154    // (https://github.com/solana-labs/solana/pull/13623), that approach was no longer viable.
155    // These tests were removed rather than rewritten to avoid accruing technical debt. Once a new
156    // rpc/client framework is implemented, they should be restored.
157    //
158    // async fn test_process_spl_token_allocations()
159    // async fn test_process_spl_token_transfer_amount_allocations()
160    // async fn test_check_spl_token_balances()
161    //
162    // https://github.com/solana-labs/solana/blob/5511d52c6284013a24ced10966d11d8f4585799e/tokens/src/spl_token.rs#L490-L685
163}