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 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), };
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 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}