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 match request_and_confirm_airdrop(rpc_client, config, &pubkey, lamports).await {
773 Ok(signature) => {
774 let signature_cli_message =
775 log_instruction_custom_error::<SystemError>(Ok(signature), config)?;
776 writeln_stdout(format_args!("{signature_cli_message}"))?;
777
778 let current_balance = rpc_client.get_balance(&pubkey).await?;
779
780 if current_balance < pre_balance.saturating_add(lamports) {
781 writeln_stdout(format_args!("Balance unchanged"))?;
782 writeln_stdout(format_args!(
783 "Run `solana confirm -v {signature:?}` for more info"
784 ))?;
785 Ok("".to_string())
786 } else {
787 Ok(build_balance_message(current_balance, false, true))
788 }
789 }
790 Err(err) => log_instruction_custom_error::<SystemError>(Err(err), config).map_err(|err| {
791 match web_faucet_url(&config.json_rpc_url, &pubkey) {
792 Some(url) => format!(
793 "{err}\n\nThe CLI faucet is closed on this cluster. Request an airdrop \
794 at:\n{url}"
795 )
796 .into(),
797 None => err,
798 }
799 }),
800 }
801}
802
803fn web_faucet_url(json_rpc_url: &str, pubkey: &Pubkey) -> Option<String> {
804 let cluster = if json_rpc_url.contains("testnet") {
805 "testnet"
806 } else if json_rpc_url.contains("devnet") {
807 "devnet"
808 } else {
809 return None;
810 };
811 Some(format!(
812 "https://faucet.solana.com/?walletAddress={pubkey}&cluster={cluster}"
813 ))
814}
815
816pub async fn process_balance(
817 rpc_client: &RpcClient,
818 config: &CliConfig<'_>,
819 pubkey: &Option<Pubkey>,
820 use_lamports_unit: bool,
821) -> ProcessResult {
822 let pubkey = if let Some(pubkey) = pubkey {
823 *pubkey
824 } else {
825 config.pubkey()?
826 };
827 let balance = rpc_client.get_balance(&pubkey).await?;
828 let balance_output = CliBalance {
829 lamports: balance,
830 config: BuildBalanceMessageConfig {
831 use_lamports_unit,
832 show_unit: true,
833 trim_trailing_zeros: true,
834 },
835 };
836
837 Ok(config.output_format.formatted_string(&balance_output))
838}
839
840pub async fn process_confirm(
841 rpc_client: &RpcClient,
842 config: &CliConfig<'_>,
843 signature: &Signature,
844) -> ProcessResult {
845 match rpc_client
846 .get_signature_statuses_with_history(&[*signature])
847 .await
848 {
849 Ok(status) => {
850 let cli_transaction = if let Some(transaction_status) = &status.value[0] {
851 let mut transaction = None;
852 let mut get_transaction_error = None;
853 if config.verbose {
854 match rpc_client
855 .get_transaction_with_config(
856 signature,
857 RpcTransactionConfig {
858 encoding: Some(UiTransactionEncoding::Base64),
859 commitment: Some(CommitmentConfig::confirmed()),
860 max_supported_transaction_version: Some(1),
861 min_context_slot: None,
862 },
863 )
864 .await
865 {
866 Ok(confirmed_transaction) => {
867 let EncodedConfirmedTransactionWithStatusMeta {
868 block_time,
869 slot,
870 transaction: transaction_with_meta,
871 ..
872 } = confirmed_transaction;
873
874 let decoded_transaction =
875 transaction_with_meta.transaction.decode().unwrap();
876 let json_transaction = decoded_transaction.json_encode();
877
878 transaction = Some(CliTransaction {
879 transaction: json_transaction,
880 meta: transaction_with_meta.meta,
881 block_time,
882 slot: Some(slot),
883 decoded_transaction,
884 prefix: " ".to_string(),
885 sigverify_status: vec![],
886 });
887 }
888 Err(err) => {
889 get_transaction_error = Some(format!("{err:?}"));
890 }
891 }
892 }
893 CliTransactionConfirmation {
894 confirmation_status: Some(transaction_status.confirmation_status()),
895 transaction,
896 get_transaction_error,
897 err: transaction_status.err.clone().map(Into::into),
898 }
899 } else {
900 CliTransactionConfirmation {
901 confirmation_status: None,
902 transaction: None,
903 get_transaction_error: None,
904 err: None,
905 }
906 };
907 Ok(config.output_format.formatted_string(&cli_transaction))
908 }
909 Err(err) => Err(CliError::RpcRequestError(format!("Unable to confirm: {err}")).into()),
910 }
911}
912
913pub fn process_decode_transaction(
914 config: &CliConfig<'_>,
915 transaction: &VersionedTransaction,
916) -> ProcessResult {
917 let sigverify_status = CliSignatureVerificationStatus::verify_transaction(transaction);
918 let decode_transaction = CliTransaction {
919 decoded_transaction: transaction.clone(),
920 transaction: transaction.json_encode(),
921 meta: None,
922 block_time: None,
923 slot: None,
924 prefix: "".to_string(),
925 sigverify_status,
926 };
927 Ok(config.output_format.formatted_string(&decode_transaction))
928}
929
930pub fn process_create_address_with_seed(
931 config: &CliConfig<'_>,
932 from_pubkey: Option<&Pubkey>,
933 seed: &str,
934 program_id: &Pubkey,
935) -> ProcessResult {
936 let from_pubkey = if let Some(pubkey) = from_pubkey {
937 *pubkey
938 } else {
939 config.pubkey()?
940 };
941 let address = Pubkey::create_with_seed(&from_pubkey, seed, program_id)?;
942 Ok(address.to_string())
943}
944
945pub fn process_find_program_derived_address(
946 config: &CliConfig<'_>,
947 seeds: &[Vec<u8>],
948 program_id: &Pubkey,
949) -> ProcessResult {
950 let seeds_slice = seeds.iter().map(|x| &x[..]).collect::<Vec<_>>();
951 let (address, bump_seed) = Pubkey::find_program_address(&seeds_slice[..], program_id);
952 let result = CliFindProgramDerivedAddress {
953 address: address.to_string(),
954 seeds: seeds.to_owned(),
955 bump_seed,
956 };
957 Ok(config.output_format.formatted_string(&result))
958}
959
960#[allow(clippy::too_many_arguments)]
961pub async fn process_transfer(
962 rpc_client: &RpcClient,
963 config: &CliConfig<'_>,
964 amount: SpendAmount,
965 to: &Pubkey,
966 from: SignerIndex,
967 sign_only: bool,
968 dump_transaction_message: bool,
969 allow_unfunded_recipient: bool,
970 no_wait: bool,
971 blockhash_query: &BlockhashQuery,
972 nonce_account: Option<&Pubkey>,
973 nonce_authority: SignerIndex,
974 memo: Option<&String>,
975 fee_payer: SignerIndex,
976 derived_address_seed: Option<String>,
977 derived_address_program_id: Option<&Pubkey>,
978 compute_unit_price: Option<u64>,
979) -> ProcessResult {
980 let from = config.signers[from];
981 let mut from_pubkey = from.pubkey();
982
983 let recent_blockhash = blockhash_query
984 .get_blockhash(rpc_client, config.commitment)
985 .await?;
986
987 if !sign_only && !allow_unfunded_recipient {
988 let recipient_balance = rpc_client
989 .get_balance_with_commitment(to, config.commitment)
990 .await?
991 .value;
992 if recipient_balance == 0 {
993 return Err(format!(
994 "The recipient address ({to}) is not funded. Add `--allow-unfunded-recipient` to \
995 complete the transfer "
996 )
997 .into());
998 }
999 }
1000
1001 let nonce_authority = config.signers[nonce_authority];
1002 let fee_payer = config.signers[fee_payer];
1003
1004 let derived_parts = derived_address_seed.zip(derived_address_program_id);
1005 let with_seed = if let Some((seed, program_id)) = derived_parts {
1006 let base_pubkey = from_pubkey;
1007 from_pubkey = Pubkey::create_with_seed(&base_pubkey, &seed, program_id)?;
1008 Some((base_pubkey, seed, program_id, from_pubkey))
1009 } else {
1010 None
1011 };
1012
1013 let compute_unit_limit = if nonce_account.is_some() {
1014 ComputeUnitLimit::Default
1015 } else {
1016 ComputeUnitLimit::Simulated
1017 };
1018 let build_message = |lamports| {
1019 let ixs = if let Some((base_pubkey, seed, program_id, from_pubkey)) = with_seed.as_ref() {
1020 vec![system_instruction::transfer_with_seed(
1021 from_pubkey,
1022 base_pubkey,
1023 seed.clone(),
1024 program_id,
1025 to,
1026 lamports,
1027 )]
1028 .with_memo(memo)
1029 .with_compute_unit_config(&ComputeUnitConfig {
1030 compute_unit_price,
1031 compute_unit_limit,
1032 })
1033 } else {
1034 vec![system_instruction::transfer(&from_pubkey, to, lamports)]
1035 .with_memo(memo)
1036 .with_compute_unit_config(&ComputeUnitConfig {
1037 compute_unit_price,
1038 compute_unit_limit,
1039 })
1040 };
1041
1042 if let Some(nonce_account) = &nonce_account {
1043 Message::new_with_nonce(
1044 ixs,
1045 Some(&fee_payer.pubkey()),
1046 nonce_account,
1047 &nonce_authority.pubkey(),
1048 )
1049 } else {
1050 Message::new(&ixs, Some(&fee_payer.pubkey()))
1051 }
1052 };
1053
1054 let (message, _) = resolve_spend_tx_and_check_account_balances(
1055 rpc_client,
1056 sign_only,
1057 amount,
1058 &recent_blockhash,
1059 &from_pubkey,
1060 &fee_payer.pubkey(),
1061 compute_unit_limit,
1062 build_message,
1063 config.commitment,
1064 )
1065 .await?;
1066 let mut tx = Transaction::new_unsigned(message);
1067
1068 if sign_only {
1069 tx.try_partial_sign(&config.signers, recent_blockhash)?;
1070 return_signers_with_config(
1071 &tx,
1072 &config.output_format,
1073 &ReturnSignersConfig {
1074 dump_transaction_message,
1075 },
1076 )
1077 } else {
1078 if let Some(nonce_account) = &nonce_account {
1079 let nonce_account =
1080 solana_rpc_client_nonce_utils::nonblocking::get_account_with_commitment(
1081 rpc_client,
1082 nonce_account,
1083 config.commitment,
1084 )
1085 .await?;
1086 check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
1087 }
1088
1089 tx.try_sign(&config.signers, recent_blockhash)?;
1090 let result = if no_wait {
1091 rpc_client
1092 .send_transaction_with_config(&tx, config.send_transaction_config)
1093 .await
1094 } else {
1095 rpc_client
1096 .send_and_confirm_transaction_with_spinner_and_config(
1097 &tx,
1098 config.commitment,
1099 config.send_transaction_config,
1100 )
1101 .await
1102 };
1103 log_instruction_custom_error::<SystemError>(result, config)
1104 }
1105}
1106
1107pub fn process_sign_offchain_message(
1108 config: &CliConfig<'_>,
1109 message: &OffchainMessage,
1110) -> ProcessResult {
1111 Ok(message.sign(config.signers[0])?.to_string())
1112}
1113
1114pub fn process_verify_offchain_signature(
1115 config: &CliConfig<'_>,
1116 signer_pubkey: &Option<Pubkey>,
1117 signature: &Signature,
1118 message: &OffchainMessage,
1119) -> ProcessResult {
1120 let signer = if let Some(pubkey) = signer_pubkey {
1121 *pubkey
1122 } else {
1123 config.signers[0].pubkey()
1124 };
1125
1126 if message.verify(&signer, signature)? {
1127 Ok("Signature is valid".to_string())
1128 } else {
1129 Err(CliError::InvalidSignature.into())
1130 }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135 use super::*;
1136
1137 #[test]
1138 fn test_web_faucet_url() {
1139 let pubkey = Pubkey::new_unique();
1140 let expected = |cluster: &str| {
1141 Some(format!(
1142 "https://faucet.solana.com/?walletAddress={pubkey}&cluster={cluster}"
1143 ))
1144 };
1145
1146 assert_eq!(
1147 web_faucet_url("https://api.devnet.solana.com", &pubkey),
1148 expected("devnet")
1149 );
1150 assert_eq!(
1151 web_faucet_url("https://api.testnet.solana.com", &pubkey),
1152 expected("testnet")
1153 );
1154 assert_eq!(
1155 web_faucet_url("https://api.mainnet-beta.solana.com", &pubkey),
1156 None
1157 );
1158 assert_eq!(web_faucet_url("http://localhost:8899", &pubkey), None);
1159 }
1160}