Skip to main content

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