1use {
2 crate::{
3 cli::{
4 CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult,
5 log_instruction_custom_error, request_and_confirm_airdrop,
6 },
7 compute_budget::{ComputeUnitConfig, WithComputeUnitConfig},
8 memo::WithMemo,
9 nonce::check_nonce_account,
10 spend_utils::{SpendAmount, resolve_spend_tx_and_check_account_balances},
11 },
12 clap::{App, Arg, ArgMatches, SubCommand, value_t_or_exit},
13 hex::FromHex,
14 solana_clap_utils::{
15 compute_budget::{COMPUTE_UNIT_PRICE_ARG, ComputeUnitLimit, compute_unit_price_arg},
16 fee_payer::*,
17 hidden_unless_forced,
18 input_parsers::*,
19 input_validators::*,
20 keypair::{DefaultSigner, SignerIndex},
21 memo::*,
22 nonce::*,
23 offline::*,
24 },
25 solana_cli_output::{
26 CliAccount, CliBalance, CliFindProgramDerivedAddress, CliSignatureVerificationStatus,
27 CliTransaction, CliTransactionConfirmation, OutputFormat, ReturnSignersConfig,
28 display::{BuildBalanceMessageConfig, build_balance_message},
29 return_signers_with_config,
30 stdout::writeln_stdout,
31 },
32 solana_commitment_config::CommitmentConfig,
33 solana_message::Message,
34 solana_offchain_message::OffchainMessage,
35 solana_pubkey::Pubkey,
36 solana_remote_wallet::remote_wallet::RemoteWalletManager,
37 solana_rpc_client::nonblocking::rpc_client::RpcClient,
38 solana_rpc_client_api::config::RpcTransactionConfig,
39 solana_rpc_client_nonce_utils::nonblocking::blockhash_query::BlockhashQuery,
40 solana_sdk_ids::{stake, system_program},
41 solana_signature::Signature,
42 solana_system_interface::{error::SystemError, instruction as system_instruction},
43 solana_transaction::{Transaction, versioned::VersionedTransaction},
44 solana_transaction_status::{
45 EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
46 TransactionBinaryEncoding, UiTransactionEncoding,
47 },
48 std::{fmt::Write as FmtWrite, fs::File, io::Write, rc::Rc, str::FromStr},
49};
50
51#[rustfmt::skip]
53const CONFIRM_AFTER_HELP_MESSAGE: &str =
54 "Note: This will show more detailed information for finalized \
55 transactions with verbose mode (-v/--verbose).\
56 \n\
57 \nAccount modes:\
58 \n |srwx|\
59 \n s: signed\
60 \n r: readable (always true)\
61 \n w: writable\
62 \n x: program account (inner instructions excluded)";
63
64#[rustfmt::skip]
65const SEEDS_ARG_HELP_MESSAGE: &str =
66 "The seeds. \n\
67 Each one must match the pattern PREFIX:VALUE. \n\
68 PREFIX can be one of [string, pubkey, hex, u8] \n\
69 or matches the pattern [u,i][16,32,64,128][le,be] \
70 (for example u64le) for number values \n\
71 [u,i] - represents whether the number is unsigned or signed, \n\
72 [16,32,64,128] - represents the bit length, and \n\
73 [le,be] - represents the byte order - little endian or big endian";
74
75pub trait WalletSubCommands {
76 fn wallet_subcommands(self) -> Self;
77}
78
79impl WalletSubCommands for App<'_, '_> {
80 fn wallet_subcommands(self) -> Self {
81 self.subcommand(
82 SubCommand::with_name("account")
83 .about("Show the contents of an account")
84 .alias("account")
85 .arg(pubkey!(
86 Arg::with_name("account_pubkey")
87 .index(1)
88 .value_name("ACCOUNT_ADDRESS")
89 .required(true),
90 "Account contents to show."
91 ))
92 .arg(
93 Arg::with_name("output_file")
94 .long("output-file")
95 .short("o")
96 .value_name("FILEPATH")
97 .takes_value(true)
98 .help("Write the account data to this file"),
99 )
100 .arg(
101 Arg::with_name("lamports")
102 .long("lamports")
103 .takes_value(false)
104 .help("Display balance in lamports instead of SOL"),
105 ),
106 )
107 .subcommand(
108 SubCommand::with_name("address")
109 .about("Get your public key")
110 .arg(
111 Arg::with_name("confirm_key")
112 .long("confirm-key")
113 .takes_value(false)
114 .help("Confirm key on device; only relevant if using remote wallet"),
115 ),
116 )
117 .subcommand(
118 SubCommand::with_name("airdrop")
119 .about("Request SOL from a faucet")
120 .arg(
121 Arg::with_name("amount")
122 .index(1)
123 .value_name("AMOUNT")
124 .takes_value(true)
125 .validator(is_amount)
126 .required(true)
127 .help("The airdrop amount to request, in SOL"),
128 )
129 .arg(pubkey!(
130 Arg::with_name("to")
131 .index(2)
132 .value_name("RECIPIENT_ADDRESS"),
133 "Account of airdrop recipient."
134 )),
135 )
136 .subcommand(
137 SubCommand::with_name("balance")
138 .about("Get your balance")
139 .arg(pubkey!(
140 Arg::with_name("pubkey")
141 .index(1)
142 .value_name("ACCOUNT_ADDRESS"),
143 "Account balance to check."
144 ))
145 .arg(
146 Arg::with_name("lamports")
147 .long("lamports")
148 .takes_value(false)
149 .help("Display balance in lamports instead of SOL"),
150 ),
151 )
152 .subcommand(
153 SubCommand::with_name("confirm")
154 .about("Confirm transaction by signature")
155 .arg(
156 Arg::with_name("signature")
157 .index(1)
158 .value_name("TRANSACTION_SIGNATURE")
159 .takes_value(true)
160 .required(true)
161 .help("The transaction signature to confirm"),
162 )
163 .after_help(CONFIRM_AFTER_HELP_MESSAGE),
164 )
165 .subcommand(
166 SubCommand::with_name("create-address-with-seed")
167 .about(
168 "Generate a derived account address with a seed. For program derived \
169 addresses (PDAs), use the find-program-derived-address command instead",
170 )
171 .arg(
172 Arg::with_name("seed")
173 .index(1)
174 .value_name("SEED_STRING")
175 .takes_value(true)
176 .required(true)
177 .validator(is_derived_address_seed)
178 .help("The seed. Must not take more than 32 bytes to encode as utf-8"),
179 )
180 .arg(
181 Arg::with_name("program_id")
182 .index(2)
183 .value_name("PROGRAM_ID")
184 .takes_value(true)
185 .required(true)
186 .help(
187 "The program_id that the address will ultimately be used for, or one \
188 of NONCE, STAKE, and VOTE keywords",
189 ),
190 )
191 .arg(pubkey!(
192 Arg::with_name("from")
193 .long("from")
194 .value_name("FROM_PUBKEY")
195 .required(false),
196 "From (base) key, [default: cli config keypair]."
197 )),
198 )
199 .subcommand(
200 SubCommand::with_name("find-program-derived-address")
201 .about("Generate a program derived account address with a seed")
202 .arg(
203 Arg::with_name("program_id")
204 .index(1)
205 .value_name("PROGRAM_ID")
206 .takes_value(true)
207 .required(true)
208 .help(
209 "The program_id that the address will ultimately be used for, or one \
210 of NONCE, STAKE, and VOTE keywords",
211 ),
212 )
213 .arg(
214 Arg::with_name("seeds")
215 .min_values(0)
216 .value_name("SEED")
217 .takes_value(true)
218 .validator(is_structured_seed)
219 .help(SEEDS_ARG_HELP_MESSAGE),
220 ),
221 )
222 .subcommand(
223 SubCommand::with_name("decode-transaction")
224 .about("Decode a serialized transaction")
225 .arg(
226 Arg::with_name("transaction")
227 .index(1)
228 .value_name("TRANSACTION")
229 .takes_value(true)
230 .required(true)
231 .help("transaction to decode"),
232 )
233 .arg(
234 Arg::with_name("encoding")
235 .index(2)
236 .value_name("ENCODING")
237 .possible_values(&["base58", "base64"]) .default_value("base58")
239 .takes_value(true)
240 .required(true)
241 .help("transaction encoding"),
242 ),
243 )
244 .subcommand(
245 SubCommand::with_name("resolve-signer")
246 .about(
247 "Checks that a signer is valid, and returns its specific path; useful for \
248 signers that may be specified generally, eg. usb://ledger",
249 )
250 .arg(
251 Arg::with_name("signer")
252 .index(1)
253 .value_name("SIGNER_KEYPAIR")
254 .takes_value(true)
255 .required(true)
256 .validator(is_valid_signer)
257 .help("The signer path to resolve"),
258 ),
259 )
260 .subcommand(
261 SubCommand::with_name("transfer")
262 .about("Transfer funds between system accounts")
263 .alias("pay")
264 .arg(pubkey!(
265 Arg::with_name("to")
266 .index(1)
267 .value_name("RECIPIENT_ADDRESS")
268 .required(true),
269 "Account of recipient."
270 ))
271 .arg(
272 Arg::with_name("amount")
273 .index(2)
274 .value_name("AMOUNT")
275 .takes_value(true)
276 .validator(is_amount_or_all)
277 .required(true)
278 .help("The amount to send, in SOL; accepts keyword ALL"),
279 )
280 .arg(pubkey!(
281 Arg::with_name("from")
282 .long("from")
283 .value_name("FROM_ADDRESS"),
284 "Source account of funds [default: cli config keypair]."
285 ))
286 .arg(
287 Arg::with_name("no_wait")
288 .long("no-wait")
289 .takes_value(false)
290 .help(
291 "Return signature immediately after submitting the transaction, \
292 instead of waiting for confirmations",
293 ),
294 )
295 .arg(
296 Arg::with_name("derived_address_seed")
297 .long("derived-address-seed")
298 .takes_value(true)
299 .value_name("SEED_STRING")
300 .requires("derived_address_program_id")
301 .validator(is_derived_address_seed)
302 .hidden(hidden_unless_forced()),
303 )
304 .arg(
305 Arg::with_name("derived_address_program_id")
306 .long("derived-address-program-id")
307 .takes_value(true)
308 .value_name("PROGRAM_ID")
309 .requires("derived_address_seed")
310 .hidden(hidden_unless_forced()),
311 )
312 .arg(
313 Arg::with_name("allow_unfunded_recipient")
314 .long("allow-unfunded-recipient")
315 .takes_value(false)
316 .help("Complete the transfer even if the recipient address is not funded"),
317 )
318 .offline_args()
319 .nonce_args(false)
320 .arg(memo_arg())
321 .arg(fee_payer_arg())
322 .arg(compute_unit_price_arg()),
323 )
324 .subcommand(
325 SubCommand::with_name("sign-offchain-message")
326 .about("Sign off-chain message")
327 .arg(
328 Arg::with_name("message")
329 .index(1)
330 .takes_value(true)
331 .value_name("STRING")
332 .required(true)
333 .help("The message text to be signed"),
334 )
335 .arg(
336 Arg::with_name("version")
337 .long("version")
338 .takes_value(true)
339 .value_name("VERSION")
340 .required(false)
341 .default_value("0")
342 .validator(|p| match p.parse::<u8>() {
343 Err(_) => Err(String::from("Must be unsigned integer")),
344 Ok(_) => Ok(()),
345 })
346 .help("The off-chain message version"),
347 ),
348 )
349 .subcommand(
350 SubCommand::with_name("verify-offchain-signature")
351 .about("Verify off-chain message signature")
352 .arg(
353 Arg::with_name("message")
354 .index(1)
355 .takes_value(true)
356 .value_name("STRING")
357 .required(true)
358 .help("The text of the original message"),
359 )
360 .arg(
361 Arg::with_name("signature")
362 .index(2)
363 .value_name("SIGNATURE")
364 .takes_value(true)
365 .required(true)
366 .help("The message signature to verify"),
367 )
368 .arg(
369 Arg::with_name("version")
370 .long("version")
371 .takes_value(true)
372 .value_name("VERSION")
373 .required(false)
374 .default_value("0")
375 .validator(|p| match p.parse::<u8>() {
376 Err(_) => Err(String::from("Must be unsigned integer")),
377 Ok(_) => Ok(()),
378 })
379 .help("The off-chain message version"),
380 )
381 .arg(pubkey!(
382 Arg::with_name("signer")
383 .long("signer")
384 .value_name("PUBKEY")
385 .required(false),
386 "Message signer [default: cli config keypair]."
387 )),
388 )
389 }
390}
391
392fn resolve_derived_address_program_id(matches: &ArgMatches<'_>, arg_name: &str) -> Option<Pubkey> {
393 matches.value_of(arg_name).and_then(|v| {
394 let upper = v.to_ascii_uppercase();
395 match upper.as_str() {
396 "NONCE" | "SYSTEM" => Some(system_program::id()),
397 "STAKE" => Some(stake::id()),
398 "VOTE" => Some(solana_vote_program::id()),
399 _ => pubkey_of(matches, arg_name),
400 }
401 })
402}
403
404pub fn parse_account(
405 matches: &ArgMatches<'_>,
406 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
407) -> Result<CliCommandInfo, CliError> {
408 let account_pubkey = pubkey_of_signer(matches, "account_pubkey", wallet_manager)?.unwrap();
409 let output_file = matches.value_of("output_file");
410 let use_lamports_unit = matches.is_present("lamports");
411 Ok(CliCommandInfo::without_signers(CliCommand::ShowAccount {
412 pubkey: account_pubkey,
413 output_file: output_file.map(ToString::to_string),
414 use_lamports_unit,
415 }))
416}
417
418pub fn parse_airdrop(
419 matches: &ArgMatches<'_>,
420 default_signer: &DefaultSigner,
421 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
422) -> Result<CliCommandInfo, CliError> {
423 let pubkey = pubkey_of_signer(matches, "to", wallet_manager)?;
424 let signers = if pubkey.is_some() {
425 vec![]
426 } else {
427 vec![default_signer.signer_from_path(matches, wallet_manager)?]
428 };
429 let lamports = lamports_of_sol(matches, "amount").unwrap();
430 Ok(CliCommandInfo {
431 command: CliCommand::Airdrop { pubkey, lamports },
432 signers,
433 })
434}
435
436pub fn parse_balance(
437 matches: &ArgMatches<'_>,
438 default_signer: &DefaultSigner,
439 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
440) -> Result<CliCommandInfo, CliError> {
441 let pubkey = pubkey_of_signer(matches, "pubkey", wallet_manager)?;
442 let signers = if pubkey.is_some() {
443 vec![]
444 } else {
445 vec![default_signer.signer_from_path(matches, wallet_manager)?]
446 };
447 Ok(CliCommandInfo {
448 command: CliCommand::Balance {
449 pubkey,
450 use_lamports_unit: matches.is_present("lamports"),
451 },
452 signers,
453 })
454}
455
456pub fn parse_decode_transaction(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
457 let blob = value_t_or_exit!(matches, "transaction", String);
458 let binary_encoding = match matches.value_of("encoding").unwrap() {
459 "base58" => TransactionBinaryEncoding::Base58,
460 "base64" => TransactionBinaryEncoding::Base64,
461 _ => unreachable!(),
462 };
463
464 let encoded_transaction = EncodedTransaction::Binary(blob, binary_encoding);
465 if let Some(transaction) = encoded_transaction.decode() {
466 Ok(CliCommandInfo::without_signers(
467 CliCommand::DecodeTransaction(transaction),
468 ))
469 } else {
470 Err(CliError::BadParameter(
471 "Unable to decode transaction".to_string(),
472 ))
473 }
474}
475
476pub fn parse_create_address_with_seed(
477 matches: &ArgMatches<'_>,
478 default_signer: &DefaultSigner,
479 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
480) -> Result<CliCommandInfo, CliError> {
481 let from_pubkey = pubkey_of_signer(matches, "from", wallet_manager)?;
482 let signers = if from_pubkey.is_some() {
483 vec![]
484 } else {
485 vec![default_signer.signer_from_path(matches, wallet_manager)?]
486 };
487
488 let program_id = resolve_derived_address_program_id(matches, "program_id").unwrap();
489
490 let seed = matches.value_of("seed").unwrap().to_string();
491
492 Ok(CliCommandInfo {
493 command: CliCommand::CreateAddressWithSeed {
494 from_pubkey,
495 seed,
496 program_id,
497 },
498 signers,
499 })
500}
501
502fn parse_structured_seed(value: &str) -> Result<Vec<u8>, CliError> {
503 let (prefix, value) = value
504 .split_once(':')
505 .ok_or_else(|| CliError::BadParameter("SEED".to_string()))?;
506
507 let invalid_seed =
508 |err: String| CliError::BadParameter(format!("Invalid seed {prefix}:{value}: {err}"));
509
510 match prefix {
511 "pubkey" => Ok(Pubkey::from_str(value)
512 .map_err(|err| invalid_seed(err.to_string()))?
513 .to_bytes()
514 .to_vec()),
515 "string" => Ok(value.as_bytes().to_vec()),
516 "hex" => Ok(Vec::<u8>::from_hex(value).map_err(|err| invalid_seed(err.to_string()))?),
517 "u8" => Ok(u8::from_str(value)
518 .map_err(|err| invalid_seed(err.to_string()))?
519 .to_le_bytes()
520 .to_vec()),
521 "u16le" => Ok(u16::from_str(value)
522 .map_err(|err| invalid_seed(err.to_string()))?
523 .to_le_bytes()
524 .to_vec()),
525 "u32le" => Ok(u32::from_str(value)
526 .map_err(|err| invalid_seed(err.to_string()))?
527 .to_le_bytes()
528 .to_vec()),
529 "u64le" => Ok(u64::from_str(value)
530 .map_err(|err| invalid_seed(err.to_string()))?
531 .to_le_bytes()
532 .to_vec()),
533 "u128le" => Ok(u128::from_str(value)
534 .map_err(|err| invalid_seed(err.to_string()))?
535 .to_le_bytes()
536 .to_vec()),
537 "i16le" => Ok(i16::from_str(value)
538 .map_err(|err| invalid_seed(err.to_string()))?
539 .to_le_bytes()
540 .to_vec()),
541 "i32le" => Ok(i32::from_str(value)
542 .map_err(|err| invalid_seed(err.to_string()))?
543 .to_le_bytes()
544 .to_vec()),
545 "i64le" => Ok(i64::from_str(value)
546 .map_err(|err| invalid_seed(err.to_string()))?
547 .to_le_bytes()
548 .to_vec()),
549 "i128le" => Ok(i128::from_str(value)
550 .map_err(|err| invalid_seed(err.to_string()))?
551 .to_le_bytes()
552 .to_vec()),
553 "u16be" => Ok(u16::from_str(value)
554 .map_err(|err| invalid_seed(err.to_string()))?
555 .to_be_bytes()
556 .to_vec()),
557 "u32be" => Ok(u32::from_str(value)
558 .map_err(|err| invalid_seed(err.to_string()))?
559 .to_be_bytes()
560 .to_vec()),
561 "u64be" => Ok(u64::from_str(value)
562 .map_err(|err| invalid_seed(err.to_string()))?
563 .to_be_bytes()
564 .to_vec()),
565 "u128be" => Ok(u128::from_str(value)
566 .map_err(|err| invalid_seed(err.to_string()))?
567 .to_be_bytes()
568 .to_vec()),
569 "i16be" => Ok(i16::from_str(value)
570 .map_err(|err| invalid_seed(err.to_string()))?
571 .to_be_bytes()
572 .to_vec()),
573 "i32be" => Ok(i32::from_str(value)
574 .map_err(|err| invalid_seed(err.to_string()))?
575 .to_be_bytes()
576 .to_vec()),
577 "i64be" => Ok(i64::from_str(value)
578 .map_err(|err| invalid_seed(err.to_string()))?
579 .to_be_bytes()
580 .to_vec()),
581 "i128be" => Ok(i128::from_str(value)
582 .map_err(|err| invalid_seed(err.to_string()))?
583 .to_be_bytes()
584 .to_vec()),
585 _ => Err(CliError::BadParameter(format!(
586 "Invalid seed prefix: {prefix}"
587 ))),
588 }
589}
590
591pub fn parse_find_program_derived_address(
592 matches: &ArgMatches<'_>,
593) -> Result<CliCommandInfo, CliError> {
594 let program_id = resolve_derived_address_program_id(matches, "program_id")
595 .ok_or_else(|| CliError::BadParameter("PROGRAM_ID".to_string()))?;
596 let seeds = matches
597 .values_of("seeds")
598 .map(|seeds| {
599 seeds
600 .map(parse_structured_seed)
601 .collect::<Result<Vec<_>, CliError>>()
602 })
603 .transpose()?
604 .unwrap_or_default();
605
606 Ok(CliCommandInfo::without_signers(
607 CliCommand::FindProgramDerivedAddress { seeds, program_id },
608 ))
609}
610
611pub fn parse_transfer(
612 matches: &ArgMatches<'_>,
613 default_signer: &DefaultSigner,
614 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
615) -> Result<CliCommandInfo, CliError> {
616 let amount = SpendAmount::new_from_matches(matches, "amount")?;
617 let to = pubkey_of_signer(matches, "to", wallet_manager)?.unwrap();
618 let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
619 let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name);
620 let no_wait = matches.is_present("no_wait");
621 let blockhash_query = BlockhashQuery::new_from_matches(matches);
622 let nonce_account = pubkey_of_signer(matches, NONCE_ARG.name, wallet_manager)?;
623 let (nonce_authority, nonce_authority_pubkey) =
624 signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?;
625 let memo = matches.value_of(MEMO_ARG.name).map(String::from);
626 let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;
627 let (from, from_pubkey) = signer_of(matches, "from", wallet_manager)?;
628 let allow_unfunded_recipient = matches.is_present("allow_unfunded_recipient");
629
630 let mut bulk_signers = vec![fee_payer, from];
631 if nonce_account.is_some() {
632 bulk_signers.push(nonce_authority);
633 }
634
635 let signer_info =
636 default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
637 let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name);
638
639 let derived_address_seed = matches
640 .value_of("derived_address_seed")
641 .map(|s| s.to_string());
642 let derived_address_program_id =
643 resolve_derived_address_program_id(matches, "derived_address_program_id");
644
645 Ok(CliCommandInfo {
646 command: CliCommand::Transfer {
647 amount,
648 to,
649 sign_only,
650 dump_transaction_message,
651 allow_unfunded_recipient,
652 no_wait,
653 blockhash_query,
654 nonce_account,
655 nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(),
656 memo,
657 fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
658 from: signer_info.index_of(from_pubkey).unwrap(),
659 derived_address_seed,
660 derived_address_program_id,
661 compute_unit_price,
662 },
663 signers: signer_info.signers,
664 })
665}
666
667pub fn parse_sign_offchain_message(
668 matches: &ArgMatches<'_>,
669 default_signer: &DefaultSigner,
670 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
671) -> Result<CliCommandInfo, CliError> {
672 let version: u8 = value_of(matches, "version").unwrap();
673 let message_text: String = value_of(matches, "message")
674 .ok_or_else(|| CliError::BadParameter("MESSAGE".to_string()))?;
675 let message = OffchainMessage::new(version, message_text.as_bytes())
676 .map_err(|_| CliError::BadParameter("VERSION or MESSAGE".to_string()))?;
677
678 Ok(CliCommandInfo {
679 command: CliCommand::SignOffchainMessage { message },
680 signers: vec![default_signer.signer_from_path(matches, wallet_manager)?],
681 })
682}
683
684pub fn parse_verify_offchain_signature(
685 matches: &ArgMatches<'_>,
686 default_signer: &DefaultSigner,
687 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
688) -> Result<CliCommandInfo, CliError> {
689 let version: u8 = value_of(matches, "version").unwrap();
690 let message_text: String = value_of(matches, "message")
691 .ok_or_else(|| CliError::BadParameter("MESSAGE".to_string()))?;
692 let message = OffchainMessage::new(version, message_text.as_bytes())
693 .map_err(|_| CliError::BadParameter("VERSION or MESSAGE".to_string()))?;
694
695 let signer_pubkey = pubkey_of_signer(matches, "signer", wallet_manager)?;
696 let signers = if signer_pubkey.is_some() {
697 vec![]
698 } else {
699 vec![default_signer.signer_from_path(matches, wallet_manager)?]
700 };
701
702 let signature = value_of(matches, "signature")
703 .ok_or_else(|| CliError::BadParameter("SIGNATURE".to_string()))?;
704
705 Ok(CliCommandInfo {
706 command: CliCommand::VerifyOffchainSignature {
707 signer_pubkey,
708 signature,
709 message,
710 },
711 signers,
712 })
713}
714
715pub async fn process_show_account(
716 rpc_client: &RpcClient,
717 config: &CliConfig<'_>,
718 account_pubkey: &Pubkey,
719 output_file: &Option<String>,
720 use_lamports_unit: bool,
721) -> ProcessResult {
722 let account = rpc_client.get_account(account_pubkey).await?;
723 let data = &account.data;
724 let cli_account = CliAccount::new(account_pubkey, &account, use_lamports_unit);
725
726 let mut account_string = config.output_format.formatted_string(&cli_account);
727
728 match config.output_format {
729 OutputFormat::Json | OutputFormat::JsonCompact => {
730 if let Some(output_file) = output_file {
731 let mut f = File::create(output_file)?;
732 f.write_all(account_string.as_bytes())?;
733 writeln!(&mut account_string)?;
734 writeln!(&mut account_string, "Wrote account to {output_file}")?;
735 }
736 }
737 OutputFormat::Display | OutputFormat::DisplayVerbose => {
738 if let Some(output_file) = output_file {
739 let mut f = File::create(output_file)?;
740 f.write_all(data)?;
741 writeln!(&mut account_string)?;
742 writeln!(&mut account_string, "Wrote account data to {output_file}")?;
743 } else if !data.is_empty() {
744 use pretty_hex::*;
745 writeln!(&mut account_string, "{:?}", data.hex_dump())?;
746 }
747 }
748 OutputFormat::DisplayQuiet => (),
749 }
750
751 Ok(account_string)
752}
753
754pub async fn process_airdrop(
755 rpc_client: &RpcClient,
756 config: &CliConfig<'_>,
757 pubkey: &Option<Pubkey>,
758 lamports: u64,
759) -> ProcessResult {
760 let pubkey = if let Some(pubkey) = pubkey {
761 *pubkey
762 } else {
763 config.pubkey()?
764 };
765 writeln_stdout(format_args!(
766 "Requesting airdrop of {}",
767 build_balance_message(lamports, false, true),
768 ))?;
769
770 let pre_balance = rpc_client.get_balance(&pubkey).await?;
771
772 let result = request_and_confirm_airdrop(rpc_client, config, &pubkey, lamports).await;
773 if let Ok(signature) = result {
774 let signature_cli_message = log_instruction_custom_error::<SystemError>(result, config)?;
775 writeln_stdout(format_args!("{signature_cli_message}"))?;
776
777 let current_balance = rpc_client.get_balance(&pubkey).await?;
778
779 if current_balance < pre_balance.saturating_add(lamports) {
780 writeln_stdout(format_args!("Balance unchanged"))?;
781 writeln_stdout(format_args!(
782 "Run `solana confirm -v {signature:?}` for more info"
783 ))?;
784 Ok("".to_string())
785 } else {
786 Ok(build_balance_message(current_balance, false, true))
787 }
788 } else {
789 log_instruction_custom_error::<SystemError>(result, config)
790 }
791}
792
793pub async fn process_balance(
794 rpc_client: &RpcClient,
795 config: &CliConfig<'_>,
796 pubkey: &Option<Pubkey>,
797 use_lamports_unit: bool,
798) -> ProcessResult {
799 let pubkey = if let Some(pubkey) = pubkey {
800 *pubkey
801 } else {
802 config.pubkey()?
803 };
804 let balance = rpc_client.get_balance(&pubkey).await?;
805 let balance_output = CliBalance {
806 lamports: balance,
807 config: BuildBalanceMessageConfig {
808 use_lamports_unit,
809 show_unit: true,
810 trim_trailing_zeros: true,
811 },
812 };
813
814 Ok(config.output_format.formatted_string(&balance_output))
815}
816
817pub async fn process_confirm(
818 rpc_client: &RpcClient,
819 config: &CliConfig<'_>,
820 signature: &Signature,
821) -> ProcessResult {
822 match rpc_client
823 .get_signature_statuses_with_history(&[*signature])
824 .await
825 {
826 Ok(status) => {
827 let cli_transaction = if let Some(transaction_status) = &status.value[0] {
828 let mut transaction = None;
829 let mut get_transaction_error = None;
830 if config.verbose {
831 match rpc_client
832 .get_transaction_with_config(
833 signature,
834 RpcTransactionConfig {
835 encoding: Some(UiTransactionEncoding::Base64),
836 commitment: Some(CommitmentConfig::confirmed()),
837 max_supported_transaction_version: Some(0),
838 },
839 )
840 .await
841 {
842 Ok(confirmed_transaction) => {
843 let EncodedConfirmedTransactionWithStatusMeta {
844 block_time,
845 slot,
846 transaction: transaction_with_meta,
847 ..
848 } = confirmed_transaction;
849
850 let decoded_transaction =
851 transaction_with_meta.transaction.decode().unwrap();
852 let json_transaction = decoded_transaction.json_encode();
853
854 transaction = Some(CliTransaction {
855 transaction: json_transaction,
856 meta: transaction_with_meta.meta,
857 block_time,
858 slot: Some(slot),
859 decoded_transaction,
860 prefix: " ".to_string(),
861 sigverify_status: vec![],
862 });
863 }
864 Err(err) => {
865 get_transaction_error = Some(format!("{err:?}"));
866 }
867 }
868 }
869 CliTransactionConfirmation {
870 confirmation_status: Some(transaction_status.confirmation_status()),
871 transaction,
872 get_transaction_error,
873 err: transaction_status.err.clone().map(Into::into),
874 }
875 } else {
876 CliTransactionConfirmation {
877 confirmation_status: None,
878 transaction: None,
879 get_transaction_error: None,
880 err: None,
881 }
882 };
883 Ok(config.output_format.formatted_string(&cli_transaction))
884 }
885 Err(err) => Err(CliError::RpcRequestError(format!("Unable to confirm: {err}")).into()),
886 }
887}
888
889pub fn process_decode_transaction(
890 config: &CliConfig<'_>,
891 transaction: &VersionedTransaction,
892) -> ProcessResult {
893 let sigverify_status = CliSignatureVerificationStatus::verify_transaction(transaction);
894 let decode_transaction = CliTransaction {
895 decoded_transaction: transaction.clone(),
896 transaction: transaction.json_encode(),
897 meta: None,
898 block_time: None,
899 slot: None,
900 prefix: "".to_string(),
901 sigverify_status,
902 };
903 Ok(config.output_format.formatted_string(&decode_transaction))
904}
905
906pub fn process_create_address_with_seed(
907 config: &CliConfig<'_>,
908 from_pubkey: Option<&Pubkey>,
909 seed: &str,
910 program_id: &Pubkey,
911) -> ProcessResult {
912 let from_pubkey = if let Some(pubkey) = from_pubkey {
913 *pubkey
914 } else {
915 config.pubkey()?
916 };
917 let address = Pubkey::create_with_seed(&from_pubkey, seed, program_id)?;
918 Ok(address.to_string())
919}
920
921pub fn process_find_program_derived_address(
922 config: &CliConfig<'_>,
923 seeds: &[Vec<u8>],
924 program_id: &Pubkey,
925) -> ProcessResult {
926 let seeds_slice = seeds.iter().map(|x| &x[..]).collect::<Vec<_>>();
927 let (address, bump_seed) = Pubkey::find_program_address(&seeds_slice[..], program_id);
928 let result = CliFindProgramDerivedAddress {
929 address: address.to_string(),
930 seeds: seeds.to_owned(),
931 bump_seed,
932 };
933 Ok(config.output_format.formatted_string(&result))
934}
935
936#[allow(clippy::too_many_arguments)]
937pub async fn process_transfer(
938 rpc_client: &RpcClient,
939 config: &CliConfig<'_>,
940 amount: SpendAmount,
941 to: &Pubkey,
942 from: SignerIndex,
943 sign_only: bool,
944 dump_transaction_message: bool,
945 allow_unfunded_recipient: bool,
946 no_wait: bool,
947 blockhash_query: &BlockhashQuery,
948 nonce_account: Option<&Pubkey>,
949 nonce_authority: SignerIndex,
950 memo: Option<&String>,
951 fee_payer: SignerIndex,
952 derived_address_seed: Option<String>,
953 derived_address_program_id: Option<&Pubkey>,
954 compute_unit_price: Option<u64>,
955) -> ProcessResult {
956 let from = config.signers[from];
957 let mut from_pubkey = from.pubkey();
958
959 let recent_blockhash = blockhash_query
960 .get_blockhash(rpc_client, config.commitment)
961 .await?;
962
963 if !sign_only && !allow_unfunded_recipient {
964 let recipient_balance = rpc_client
965 .get_balance_with_commitment(to, config.commitment)
966 .await?
967 .value;
968 if recipient_balance == 0 {
969 return Err(format!(
970 "The recipient address ({to}) is not funded. Add `--allow-unfunded-recipient` to \
971 complete the transfer "
972 )
973 .into());
974 }
975 }
976
977 let nonce_authority = config.signers[nonce_authority];
978 let fee_payer = config.signers[fee_payer];
979
980 let derived_parts = derived_address_seed.zip(derived_address_program_id);
981 let with_seed = if let Some((seed, program_id)) = derived_parts {
982 let base_pubkey = from_pubkey;
983 from_pubkey = Pubkey::create_with_seed(&base_pubkey, &seed, program_id)?;
984 Some((base_pubkey, seed, program_id, from_pubkey))
985 } else {
986 None
987 };
988
989 let compute_unit_limit = if nonce_account.is_some() {
990 ComputeUnitLimit::Default
991 } else {
992 ComputeUnitLimit::Simulated
993 };
994 let build_message = |lamports| {
995 let ixs = if let Some((base_pubkey, seed, program_id, from_pubkey)) = with_seed.as_ref() {
996 vec![system_instruction::transfer_with_seed(
997 from_pubkey,
998 base_pubkey,
999 seed.clone(),
1000 program_id,
1001 to,
1002 lamports,
1003 )]
1004 .with_memo(memo)
1005 .with_compute_unit_config(&ComputeUnitConfig {
1006 compute_unit_price,
1007 compute_unit_limit,
1008 })
1009 } else {
1010 vec![system_instruction::transfer(&from_pubkey, to, lamports)]
1011 .with_memo(memo)
1012 .with_compute_unit_config(&ComputeUnitConfig {
1013 compute_unit_price,
1014 compute_unit_limit,
1015 })
1016 };
1017
1018 if let Some(nonce_account) = &nonce_account {
1019 Message::new_with_nonce(
1020 ixs,
1021 Some(&fee_payer.pubkey()),
1022 nonce_account,
1023 &nonce_authority.pubkey(),
1024 )
1025 } else {
1026 Message::new(&ixs, Some(&fee_payer.pubkey()))
1027 }
1028 };
1029
1030 let (message, _) = resolve_spend_tx_and_check_account_balances(
1031 rpc_client,
1032 sign_only,
1033 amount,
1034 &recent_blockhash,
1035 &from_pubkey,
1036 &fee_payer.pubkey(),
1037 compute_unit_limit,
1038 build_message,
1039 config.commitment,
1040 )
1041 .await?;
1042 let mut tx = Transaction::new_unsigned(message);
1043
1044 if sign_only {
1045 tx.try_partial_sign(&config.signers, recent_blockhash)?;
1046 return_signers_with_config(
1047 &tx,
1048 &config.output_format,
1049 &ReturnSignersConfig {
1050 dump_transaction_message,
1051 },
1052 )
1053 } else {
1054 if let Some(nonce_account) = &nonce_account {
1055 let nonce_account =
1056 solana_rpc_client_nonce_utils::nonblocking::get_account_with_commitment(
1057 rpc_client,
1058 nonce_account,
1059 config.commitment,
1060 )
1061 .await?;
1062 check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
1063 }
1064
1065 tx.try_sign(&config.signers, recent_blockhash)?;
1066 let result = if no_wait {
1067 rpc_client
1068 .send_transaction_with_config(&tx, config.send_transaction_config)
1069 .await
1070 } else {
1071 rpc_client
1072 .send_and_confirm_transaction_with_spinner_and_config(
1073 &tx,
1074 config.commitment,
1075 config.send_transaction_config,
1076 )
1077 .await
1078 };
1079 log_instruction_custom_error::<SystemError>(result, config)
1080 }
1081}
1082
1083pub fn process_sign_offchain_message(
1084 config: &CliConfig<'_>,
1085 message: &OffchainMessage,
1086) -> ProcessResult {
1087 Ok(message.sign(config.signers[0])?.to_string())
1088}
1089
1090pub fn process_verify_offchain_signature(
1091 config: &CliConfig<'_>,
1092 signer_pubkey: &Option<Pubkey>,
1093 signature: &Signature,
1094 message: &OffchainMessage,
1095) -> ProcessResult {
1096 let signer = if let Some(pubkey) = signer_pubkey {
1097 *pubkey
1098 } else {
1099 config.signers[0].pubkey()
1100 };
1101
1102 if message.verify(&signer, signature)? {
1103 Ok("Signature is valid".to_string())
1104 } else {
1105 Err(CliError::InvalidSignature.into())
1106 }
1107}