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