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