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