solana_cli/
spend_utils.rs

1use {
2    crate::{
3        checks::{check_account_for_balance_with_commitment, get_fee_for_messages},
4        cli::CliError,
5        compute_budget::{simulate_and_update_compute_unit_limit, UpdateComputeUnitLimitResult},
6    },
7    clap::ArgMatches,
8    solana_clap_utils::{
9        compute_budget::ComputeUnitLimit, input_parsers::lamports_of_sol, offline::SIGN_ONLY_ARG,
10    },
11    solana_commitment_config::CommitmentConfig,
12    solana_hash::Hash,
13    solana_message::Message,
14    solana_native_token::lamports_to_sol,
15    solana_pubkey::Pubkey,
16    solana_rpc_client::rpc_client::RpcClient,
17};
18
19#[derive(Debug, PartialEq, Eq, Clone, Copy)]
20pub enum SpendAmount {
21    All,
22    Some(u64),
23    RentExempt,
24    AllForAccountCreation { create_account_min_balance: u64 },
25}
26
27impl Default for SpendAmount {
28    fn default() -> Self {
29        Self::Some(u64::default())
30    }
31}
32
33impl SpendAmount {
34    pub fn new(amount: Option<u64>, sign_only: bool) -> Self {
35        match amount {
36            Some(lamports) => Self::Some(lamports),
37            None if !sign_only => Self::All,
38            _ => panic!("ALL amount not supported for sign-only operations"),
39        }
40    }
41
42    pub fn new_from_matches(matches: &ArgMatches<'_>, name: &str) -> Self {
43        let amount = lamports_of_sol(matches, name);
44        let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
45        SpendAmount::new(amount, sign_only)
46    }
47}
48
49struct SpendAndFee {
50    spend: u64,
51    fee: u64,
52}
53
54pub fn resolve_spend_tx_and_check_account_balance<F>(
55    rpc_client: &RpcClient,
56    sign_only: bool,
57    amount: SpendAmount,
58    blockhash: &Hash,
59    from_pubkey: &Pubkey,
60    compute_unit_limit: ComputeUnitLimit,
61    build_message: F,
62    commitment: CommitmentConfig,
63) -> Result<(Message, u64), CliError>
64where
65    F: Fn(u64) -> Message,
66{
67    resolve_spend_tx_and_check_account_balances(
68        rpc_client,
69        sign_only,
70        amount,
71        blockhash,
72        from_pubkey,
73        from_pubkey,
74        compute_unit_limit,
75        build_message,
76        commitment,
77    )
78}
79
80pub fn resolve_spend_tx_and_check_account_balances<F>(
81    rpc_client: &RpcClient,
82    sign_only: bool,
83    amount: SpendAmount,
84    blockhash: &Hash,
85    from_pubkey: &Pubkey,
86    fee_pubkey: &Pubkey,
87    compute_unit_limit: ComputeUnitLimit,
88    build_message: F,
89    commitment: CommitmentConfig,
90) -> Result<(Message, u64), CliError>
91where
92    F: Fn(u64) -> Message,
93{
94    if sign_only {
95        let (message, SpendAndFee { spend, fee: _ }) = resolve_spend_message(
96            rpc_client,
97            amount,
98            None,
99            0,
100            from_pubkey,
101            fee_pubkey,
102            0,
103            compute_unit_limit,
104            build_message,
105        )?;
106        Ok((message, spend))
107    } else {
108        let from_balance = rpc_client
109            .get_balance_with_commitment(from_pubkey, commitment)?
110            .value;
111        let from_rent_exempt_minimum = if amount == SpendAmount::RentExempt {
112            let data = rpc_client.get_account_data(from_pubkey)?;
113            rpc_client.get_minimum_balance_for_rent_exemption(data.len())?
114        } else {
115            0
116        };
117        let (message, SpendAndFee { spend, fee }) = resolve_spend_message(
118            rpc_client,
119            amount,
120            Some(blockhash),
121            from_balance,
122            from_pubkey,
123            fee_pubkey,
124            from_rent_exempt_minimum,
125            compute_unit_limit,
126            build_message,
127        )?;
128        if from_pubkey == fee_pubkey {
129            if from_balance == 0 || from_balance < spend.saturating_add(fee) {
130                return Err(CliError::InsufficientFundsForSpendAndFee(
131                    lamports_to_sol(spend),
132                    lamports_to_sol(fee),
133                    *from_pubkey,
134                ));
135            }
136        } else {
137            if from_balance < spend {
138                return Err(CliError::InsufficientFundsForSpend(
139                    lamports_to_sol(spend),
140                    *from_pubkey,
141                ));
142            }
143            if !check_account_for_balance_with_commitment(rpc_client, fee_pubkey, fee, commitment)?
144            {
145                return Err(CliError::InsufficientFundsForFee(
146                    lamports_to_sol(fee),
147                    *fee_pubkey,
148                ));
149            }
150        }
151        Ok((message, spend))
152    }
153}
154
155fn resolve_spend_message<F>(
156    rpc_client: &RpcClient,
157    amount: SpendAmount,
158    blockhash: Option<&Hash>,
159    from_balance: u64,
160    from_pubkey: &Pubkey,
161    fee_pubkey: &Pubkey,
162    from_rent_exempt_minimum: u64,
163    compute_unit_limit: ComputeUnitLimit,
164    build_message: F,
165) -> Result<(Message, SpendAndFee), CliError>
166where
167    F: Fn(u64) -> Message,
168{
169    let (fee, compute_unit_info) = match blockhash {
170        Some(blockhash) => {
171            // If the from account is the same as the fee payer, it's impossible
172            // to give a correct amount for the simulation with `SpendAmount::All`
173            // or `SpendAmount::RentExempt`.
174            // To know how much to transfer, we need to know the transaction fee,
175            // but the transaction fee is dependent on the amount of compute
176            // units used, which requires simulation.
177            // To get around this limitation, we simulate against an amount of
178            // `0`, since there are few situations in which `SpendAmount` can
179            // be `All` or `RentExempt` *and also* the from account is the fee
180            // payer.
181            let lamports = if from_pubkey == fee_pubkey {
182                match amount {
183                    SpendAmount::Some(lamports) => lamports,
184                    SpendAmount::AllForAccountCreation {
185                        create_account_min_balance,
186                    } => create_account_min_balance,
187                    SpendAmount::All | SpendAmount::RentExempt => 0,
188                }
189            } else {
190                match amount {
191                    SpendAmount::Some(lamports) => lamports,
192                    SpendAmount::AllForAccountCreation { .. } | SpendAmount::All => from_balance,
193                    SpendAmount::RentExempt => {
194                        from_balance.saturating_sub(from_rent_exempt_minimum)
195                    }
196                }
197            };
198            let mut dummy_message = build_message(lamports);
199
200            dummy_message.recent_blockhash = *blockhash;
201            let compute_unit_info =
202                if let UpdateComputeUnitLimitResult::UpdatedInstructionIndex(ix_index) =
203                    simulate_and_update_compute_unit_limit(
204                        &compute_unit_limit,
205                        rpc_client,
206                        &mut dummy_message,
207                    )?
208                {
209                    Some((ix_index, dummy_message.instructions[ix_index].data.clone()))
210                } else {
211                    None
212                };
213            (
214                get_fee_for_messages(rpc_client, &[&dummy_message])?,
215                compute_unit_info,
216            )
217        }
218        None => (0, None), // Offline, cannot calculate fee
219    };
220
221    let (mut message, spend_and_fee) = match amount {
222        SpendAmount::Some(lamports) => (
223            build_message(lamports),
224            SpendAndFee {
225                spend: lamports,
226                fee,
227            },
228        ),
229        SpendAmount::All | SpendAmount::AllForAccountCreation { .. } => {
230            let lamports = if from_pubkey == fee_pubkey {
231                from_balance.saturating_sub(fee)
232            } else {
233                from_balance
234            };
235            (
236                build_message(lamports),
237                SpendAndFee {
238                    spend: lamports,
239                    fee,
240                },
241            )
242        }
243        SpendAmount::RentExempt => {
244            let mut lamports = if from_pubkey == fee_pubkey {
245                from_balance.saturating_sub(fee)
246            } else {
247                from_balance
248            };
249            lamports = lamports.saturating_sub(from_rent_exempt_minimum);
250            (
251                build_message(lamports),
252                SpendAndFee {
253                    spend: lamports,
254                    fee,
255                },
256            )
257        }
258    };
259    // After build message, update with correct compute units
260    if let Some((ix_index, ix_data)) = compute_unit_info {
261        message.instructions[ix_index].data = ix_data;
262    }
263    Ok((message, spend_and_fee))
264}