Skip to main content

solana_cli/
wallet.rs

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// Formatted specifically for the manually-indented heredoc string
52#[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"]) // Variants of `TransactionBinaryEncoding` enum
238                        .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}