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