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