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, vote,
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::RentExempt && account.owner == solana_sdk_ids::vote::id() {
138 let pending_delegator_rewards =
141 vote::get_pending_delegator_rewards(&account, from_pubkey)?;
142 from_balance = from_balance.saturating_sub(pending_delegator_rewards);
143 }
144 if amount == SpendAmount::Available && account.owner == solana_sdk_ids::stake::id() {
145 let state = stake::get_account_stake_state(
146 rpc_client,
147 from_pubkey,
148 account,
149 true,
150 None,
151 false,
152 None,
153 )
154 .await?;
155 let mut subtract_rent_exempt_minimum = false;
156 if let Some(active_stake) = state.active_stake {
157 from_balance = from_balance.saturating_sub(active_stake);
158 subtract_rent_exempt_minimum = true;
159 }
160 if let Some(activating_stake) = state.activating_stake {
161 from_balance = from_balance.saturating_sub(activating_stake);
162 subtract_rent_exempt_minimum = true;
163 }
164 if subtract_rent_exempt_minimum {
165 from_balance = from_balance.saturating_sub(from_rent_exempt_minimum);
166 }
167 }
168 let (message, SpendAndFee { spend, fee }) = resolve_spend_message(
169 rpc_client,
170 amount,
171 Some(blockhash),
172 from_balance,
173 from_pubkey,
174 fee_pubkey,
175 from_rent_exempt_minimum,
176 compute_unit_limit,
177 build_message,
178 )
179 .await?;
180 if from_pubkey == fee_pubkey {
181 if from_balance == 0 || from_balance < spend.saturating_add(fee) {
182 return Err(CliError::InsufficientFundsForSpendAndFee(
183 build_balance_message(spend, false, false),
184 build_balance_message(fee, false, false),
185 *from_pubkey,
186 ));
187 }
188 } else {
189 if from_balance < spend {
190 return Err(CliError::InsufficientFundsForSpend(
191 build_balance_message(spend, false, false),
192 *from_pubkey,
193 ));
194 }
195 if !check_account_for_balance_with_commitment(rpc_client, fee_pubkey, fee, commitment)
196 .await?
197 {
198 return Err(CliError::InsufficientFundsForFee(
199 build_balance_message(fee, false, false),
200 *fee_pubkey,
201 ));
202 }
203 }
204 Ok((message, spend))
205 }
206}
207
208async fn resolve_spend_message<F>(
209 rpc_client: &RpcClient,
210 amount: SpendAmount,
211 blockhash: Option<&Hash>,
212 from_account_transferable_balance: u64,
213 from_pubkey: &Pubkey,
214 fee_pubkey: &Pubkey,
215 from_rent_exempt_minimum: u64,
216 compute_unit_limit: ComputeUnitLimit,
217 build_message: F,
218) -> Result<(Message, SpendAndFee), CliError>
219where
220 F: Fn(u64) -> Message,
221{
222 let (fee, compute_unit_info) = match blockhash {
223 Some(blockhash) => {
224 let lamports = if from_pubkey == fee_pubkey {
235 match amount {
236 SpendAmount::Some(lamports) => lamports,
237 SpendAmount::AllForAccountCreation {
238 create_account_min_balance,
239 } => create_account_min_balance,
240 SpendAmount::All | SpendAmount::Available | SpendAmount::RentExempt => 0,
241 }
242 } else {
243 match amount {
244 SpendAmount::Some(lamports) => lamports,
245 SpendAmount::AllForAccountCreation { .. }
246 | SpendAmount::All
247 | SpendAmount::Available => from_account_transferable_balance,
248 SpendAmount::RentExempt => {
249 from_account_transferable_balance.saturating_sub(from_rent_exempt_minimum)
250 }
251 }
252 };
253 let mut dummy_message = build_message(lamports);
254
255 dummy_message.recent_blockhash = *blockhash;
256 let compute_unit_info =
257 if let UpdateComputeUnitLimitResult::UpdatedInstructionIndex(ix_index) =
258 simulate_and_update_compute_unit_limit(
259 &compute_unit_limit,
260 rpc_client,
261 &mut dummy_message,
262 )
263 .await?
264 {
265 Some((ix_index, dummy_message.instructions[ix_index].data.clone()))
266 } else {
267 None
268 };
269 (
270 get_fee_for_messages(rpc_client, &[&dummy_message]).await?,
271 compute_unit_info,
272 )
273 }
274 None => (0, None), };
276
277 let (mut message, spend_and_fee) = match amount {
278 SpendAmount::Some(lamports) => (
279 build_message(lamports),
280 SpendAndFee {
281 spend: lamports,
282 fee,
283 },
284 ),
285 SpendAmount::All | SpendAmount::AllForAccountCreation { .. } | SpendAmount::Available => {
286 let lamports = if from_pubkey == fee_pubkey {
287 from_account_transferable_balance.saturating_sub(fee)
288 } else {
289 from_account_transferable_balance
290 };
291 (
292 build_message(lamports),
293 SpendAndFee {
294 spend: lamports,
295 fee,
296 },
297 )
298 }
299 SpendAmount::RentExempt => {
300 let mut lamports = if from_pubkey == fee_pubkey {
301 from_account_transferable_balance.saturating_sub(fee)
302 } else {
303 from_account_transferable_balance
304 };
305 lamports = lamports.saturating_sub(from_rent_exempt_minimum);
306 (
307 build_message(lamports),
308 SpendAndFee {
309 spend: lamports,
310 fee,
311 },
312 )
313 }
314 };
315 if let Some((ix_index, ix_data)) = compute_unit_info {
317 message.instructions[ix_index].data = ix_data;
318 }
319 Ok((message, spend_and_fee))
320}
321
322#[cfg(test)]
323mod tests {
324 use {
325 super::*,
326 serde_json::json,
327 solana_account::Account,
328 solana_account_decoder::{UiAccountEncoding, encode_ui_account},
329 solana_rpc_client_api::{
330 request::RpcRequest,
331 response::{Response, RpcResponseContext},
332 },
333 solana_vote_program::{
334 vote_instruction::withdraw,
335 vote_state::{VoteStateV4, VoteStateVersions},
336 },
337 std::collections::HashMap,
338 };
339
340 fn vote_account(lamports: u64, pending_delegator_rewards: u64) -> Account {
341 let vote_state = VoteStateV4 {
342 pending_delegator_rewards,
343 ..VoteStateV4::default()
344 };
345 let mut data = vec![0; VoteStateV4::size_of()];
346 VoteStateV4::serialize(&VoteStateVersions::new_v4(vote_state), &mut data).unwrap();
347 Account {
348 lamports,
349 data,
350 owner: solana_sdk_ids::vote::id(),
351 ..Account::default()
352 }
353 }
354
355 async fn resolve_rent_exempt_spend(vote_account: Account, rent_exempt_minimum: u64) -> u64 {
358 let vote_account_pubkey = Pubkey::new_unique();
359 let account_response = json!(Response {
360 context: RpcResponseContext {
361 slot: 1,
362 api_version: None
363 },
364 value: json!(encode_ui_account(
365 &vote_account_pubkey,
366 &vote_account,
367 UiAccountEncoding::Base64,
368 None,
369 None,
370 )),
371 });
372 let mut mocks = HashMap::new();
373 mocks.insert(RpcRequest::GetAccountInfo, account_response);
374 mocks.insert(
375 RpcRequest::GetMinimumBalanceForRentExemption,
376 json!(rent_exempt_minimum),
377 );
378 let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
379
380 let withdraw_authority = Pubkey::new_unique();
381 let destination_pubkey = Pubkey::new_unique();
382 let fee_payer = Pubkey::new_unique();
383 let (_message, spend) = resolve_spend_tx_and_check_account_balances(
384 &rpc_client,
385 false,
386 SpendAmount::RentExempt,
387 &Hash::default(),
388 &vote_account_pubkey,
389 &fee_payer,
390 ComputeUnitLimit::Default,
391 |lamports| {
392 Message::new(
393 &[withdraw(
394 &vote_account_pubkey,
395 &withdraw_authority,
396 lamports,
397 &destination_pubkey,
398 )],
399 Some(&fee_payer),
400 )
401 },
402 CommitmentConfig::processed(),
403 )
404 .await
405 .unwrap();
406 spend
407 }
408
409 #[tokio::test]
410 async fn test_rent_exempt_spend_reserves_pending_delegator_rewards() {
411 const RENT_EXEMPT_MINIMUM: u64 = 27_000_000;
412 const PENDING_DELEGATOR_REWARDS: u64 = 5_000_000;
413 const WITHDRAWABLE: u64 = 1_000_000;
414 const RESERVED_BALANCE: u64 = RENT_EXEMPT_MINIMUM + PENDING_DELEGATOR_REWARDS;
415 const BALANCE: u64 = RESERVED_BALANCE + WITHDRAWABLE;
416 const BALANCE_WITHOUT_PENDING_REWARDS: u64 = RENT_EXEMPT_MINIMUM + WITHDRAWABLE;
417
418 assert_eq!(
421 resolve_rent_exempt_spend(
422 vote_account(BALANCE_WITHOUT_PENDING_REWARDS, 0),
423 RENT_EXEMPT_MINIMUM,
424 )
425 .await,
426 WITHDRAWABLE
427 );
428
429 assert_eq!(
432 resolve_rent_exempt_spend(
433 vote_account(BALANCE, PENDING_DELEGATOR_REWARDS),
434 RENT_EXEMPT_MINIMUM,
435 )
436 .await,
437 WITHDRAWABLE
438 );
439
440 assert_eq!(
442 resolve_rent_exempt_spend(
443 vote_account(RESERVED_BALANCE, PENDING_DELEGATOR_REWARDS),
444 RENT_EXEMPT_MINIMUM,
445 )
446 .await,
447 0
448 );
449 }
450}