solana/cli/src/wallet.rs

1008 lines
38 KiB
Rust

use {
crate::{
cli::{
log_instruction_custom_error, request_and_confirm_airdrop, CliCommand, CliCommandInfo,
CliConfig, CliError, ProcessResult,
},
compute_unit_price::WithComputeUnitPrice,
memo::WithMemo,
nonce::check_nonce_account,
spend_utils::{resolve_spend_tx_and_check_account_balances, SpendAmount},
},
clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand},
hex::FromHex,
solana_clap_utils::{
compute_unit_price::{compute_unit_price_arg, COMPUTE_UNIT_PRICE_ARG},
fee_payer::*,
hidden_unless_forced,
input_parsers::*,
input_validators::*,
keypair::{DefaultSigner, SignerIndex},
memo::*,
nonce::*,
offline::*,
},
solana_cli_output::{
display::{build_balance_message, BuildBalanceMessageConfig},
return_signers_with_config, CliAccount, CliBalance, CliFindProgramDerivedAddress,
CliSignatureVerificationStatus, CliTransaction, CliTransactionConfirmation, OutputFormat,
ReturnSignersConfig,
},
solana_remote_wallet::remote_wallet::RemoteWalletManager,
solana_rpc_client::rpc_client::RpcClient,
solana_rpc_client_api::config::RpcTransactionConfig,
solana_rpc_client_nonce_utils::blockhash_query::BlockhashQuery,
solana_sdk::{
commitment_config::CommitmentConfig,
message::Message,
offchain_message::OffchainMessage,
pubkey::Pubkey,
signature::Signature,
stake,
system_instruction::{self, SystemError},
system_program,
transaction::{Transaction, VersionedTransaction},
},
solana_transaction_status::{
EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
TransactionBinaryEncoding, UiTransactionEncoding,
},
std::{fmt::Write as FmtWrite, fs::File, io::Write, str::FromStr, sync::Arc},
};
pub trait WalletSubCommands {
fn wallet_subcommands(self) -> Self;
}
impl WalletSubCommands for App<'_, '_> {
fn wallet_subcommands(self) -> Self {
self.subcommand(
SubCommand::with_name("account")
.about("Show the contents of an account")
.alias("account")
.arg(
pubkey!(Arg::with_name("account_pubkey")
.index(1)
.value_name("ACCOUNT_ADDRESS")
.required(true),
"Account key URI. ")
)
.arg(
Arg::with_name("output_file")
.long("output-file")
.short("o")
.value_name("FILEPATH")
.takes_value(true)
.help("Write the account data to this file"),
)
.arg(
Arg::with_name("lamports")
.long("lamports")
.takes_value(false)
.help("Display balance in lamports instead of SOL"),
),
)
.subcommand(
SubCommand::with_name("address")
.about("Get your public key")
.arg(
Arg::with_name("confirm_key")
.long("confirm-key")
.takes_value(false)
.help("Confirm key on device; only relevant if using remote wallet"),
),
)
.subcommand(
SubCommand::with_name("airdrop")
.about("Request SOL from a faucet")
.arg(
Arg::with_name("amount")
.index(1)
.value_name("AMOUNT")
.takes_value(true)
.validator(is_amount)
.required(true)
.help("The airdrop amount to request, in SOL"),
)
.arg(
pubkey!(Arg::with_name("to")
.index(2)
.value_name("RECIPIENT_ADDRESS"),
"The account address of airdrop recipient. "),
),
)
.subcommand(
SubCommand::with_name("balance")
.about("Get your balance")
.arg(
pubkey!(Arg::with_name("pubkey")
.index(1)
.value_name("ACCOUNT_ADDRESS"),
"The account address of the balance to check. ")
)
.arg(
Arg::with_name("lamports")
.long("lamports")
.takes_value(false)
.help("Display balance in lamports instead of SOL"),
),
)
.subcommand(
SubCommand::with_name("confirm")
.about("Confirm transaction by signature")
.arg(
Arg::with_name("signature")
.index(1)
.value_name("TRANSACTION_SIGNATURE")
.takes_value(true)
.required(true)
.help("The transaction signature to confirm"),
)
.after_help(// Formatted specifically for the manually-indented heredoc string
"Note: This will show more detailed information for finalized transactions with verbose mode (-v/--verbose).\
\n\
\nAccount modes:\
\n |srwx|\
\n s: signed\
\n r: readable (always true)\
\n w: writable\
\n x: program account (inner instructions excluded)\
"
),
)
.subcommand(
SubCommand::with_name("create-address-with-seed")
.about(
"Generate a derived account address with a seed. \
For program derived addresses (PDAs), use the find-program-derived-address command instead"
)
.arg(
Arg::with_name("seed")
.index(1)
.value_name("SEED_STRING")
.takes_value(true)
.required(true)
.validator(is_derived_address_seed)
.help("The seed. Must not take more than 32 bytes to encode as utf-8"),
)
.arg(
Arg::with_name("program_id")
.index(2)
.value_name("PROGRAM_ID")
.takes_value(true)
.required(true)
.help(
"The program_id that the address will ultimately be used for, \n\
or one of NONCE, STAKE, and VOTE keywords",
),
)
.arg(
pubkey!(Arg::with_name("from")
.long("from")
.value_name("FROM_PUBKEY")
.required(false),
"From (base) key, [default: cli config keypair]. "),
),
)
.subcommand(
SubCommand::with_name("find-program-derived-address")
.about("Generate a program derived account address with a seed")
.arg(
Arg::with_name("program_id")
.index(1)
.value_name("PROGRAM_ID")
.takes_value(true)
.required(true)
.help(
"The program_id that the address will ultimately be used for, \n\
or one of NONCE, STAKE, and VOTE keywords",
),
)
.arg(
Arg::with_name("seeds")
.min_values(0)
.value_name("SEED")
.takes_value(true)
.validator(is_structured_seed)
.help(
"The seeds. \n\
Each one must match the pattern PREFIX:VALUE. \n\
PREFIX can be one of [string, pubkey, hex, u8] \n\
or matches the pattern [u,i][16,32,64,128][le,be] (for example u64le) for number values \n\
[u,i] - represents whether the number is unsigned or signed, \n\
[16,32,64,128] - represents the bit length, and \n\
[le,be] - represents the byte order - little endian or big endian"
),
),
)
.subcommand(
SubCommand::with_name("decode-transaction")
.about("Decode a serialized transaction")
.arg(
Arg::with_name("transaction")
.index(1)
.value_name("TRANSACTION")
.takes_value(true)
.required(true)
.help("transaction to decode"),
)
.arg(
Arg::with_name("encoding")
.index(2)
.value_name("ENCODING")
.possible_values(&["base58", "base64"]) // Variants of `TransactionBinaryEncoding` enum
.default_value("base58")
.takes_value(true)
.required(true)
.help("transaction encoding"),
),
)
.subcommand(
SubCommand::with_name("resolve-signer")
.about("Checks that a signer is valid, and returns its specific path; useful for signers that may be specified generally, eg. usb://ledger")
.arg(
Arg::with_name("signer")
.index(1)
.value_name("SIGNER_KEYPAIR")
.takes_value(true)
.required(true)
.validator(is_valid_signer)
.help("The signer path to resolve")
)
)
.subcommand(
SubCommand::with_name("transfer")
.about("Transfer funds between system accounts")
.alias("pay")
.arg(
pubkey!(Arg::with_name("to")
.index(1)
.value_name("RECIPIENT_ADDRESS")
.required(true),
"The account address of recipient. "),
)
.arg(
Arg::with_name("amount")
.index(2)
.value_name("AMOUNT")
.takes_value(true)
.validator(is_amount_or_all)
.required(true)
.help("The amount to send, in SOL; accepts keyword ALL"),
)
.arg(
pubkey!(Arg::with_name("from")
.long("from")
.value_name("FROM_ADDRESS"),
"Source account of funds (if different from client local account). "),
)
.arg(
Arg::with_name("no_wait")
.long("no-wait")
.takes_value(false)
.help("Return signature immediately after submitting the transaction, instead of waiting for confirmations"),
)
.arg(
Arg::with_name("derived_address_seed")
.long("derived-address-seed")
.takes_value(true)
.value_name("SEED_STRING")
.requires("derived_address_program_id")
.validator(is_derived_address_seed)
.hidden(hidden_unless_forced())
)
.arg(
Arg::with_name("derived_address_program_id")
.long("derived-address-program-id")
.takes_value(true)
.value_name("PROGRAM_ID")
.requires("derived_address_seed")
.hidden(hidden_unless_forced())
)
.arg(
Arg::with_name("allow_unfunded_recipient")
.long("allow-unfunded-recipient")
.takes_value(false)
.help("Complete the transfer even if the recipient address is not funded")
)
.offline_args()
.nonce_args(false)
.arg(memo_arg())
.arg(fee_payer_arg())
.arg(compute_unit_price_arg()),
)
.subcommand(
SubCommand::with_name("sign-offchain-message")
.about("Sign off-chain message")
.arg(
Arg::with_name("message")
.index(1)
.takes_value(true)
.value_name("STRING")
.required(true)
.help("The message text to be signed")
)
.arg(
Arg::with_name("version")
.long("version")
.takes_value(true)
.value_name("VERSION")
.required(false)
.default_value("0")
.validator(|p| match p.parse::<u8>() {
Err(_) => Err(String::from("Must be unsigned integer")),
Ok(_) => { Ok(()) }
})
.help("The off-chain message version")
)
)
.subcommand(
SubCommand::with_name("verify-offchain-signature")
.about("Verify off-chain message signature")
.arg(
Arg::with_name("message")
.index(1)
.takes_value(true)
.value_name("STRING")
.required(true)
.help("The text of the original message")
)
.arg(
Arg::with_name("signature")
.index(2)
.value_name("SIGNATURE")
.takes_value(true)
.required(true)
.help("The message signature to verify")
)
.arg(
Arg::with_name("version")
.long("version")
.takes_value(true)
.value_name("VERSION")
.required(false)
.default_value("0")
.validator(|p| match p.parse::<u8>() {
Err(_) => Err(String::from("Must be unsigned integer")),
Ok(_) => { Ok(()) }
})
.help("The off-chain message version")
)
.arg(
pubkey!(Arg::with_name("signer")
.long("signer")
.value_name("PUBKEY")
.required(false),
"The pubkey of the message signer (if different from config default)")
)
)
}
}
fn resolve_derived_address_program_id(matches: &ArgMatches<'_>, arg_name: &str) -> Option<Pubkey> {
matches.value_of(arg_name).and_then(|v| {
let upper = v.to_ascii_uppercase();
match upper.as_str() {
"NONCE" | "SYSTEM" => Some(system_program::id()),
"STAKE" => Some(stake::program::id()),
"VOTE" => Some(solana_vote_program::id()),
_ => pubkey_of(matches, arg_name),
}
})
}
pub fn parse_account(
matches: &ArgMatches<'_>,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let account_pubkey = pubkey_of_signer(matches, "account_pubkey", wallet_manager)?.unwrap();
let output_file = matches.value_of("output_file");
let use_lamports_unit = matches.is_present("lamports");
Ok(CliCommandInfo {
command: CliCommand::ShowAccount {
pubkey: account_pubkey,
output_file: output_file.map(ToString::to_string),
use_lamports_unit,
},
signers: vec![],
})
}
pub fn parse_airdrop(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let pubkey = pubkey_of_signer(matches, "to", wallet_manager)?;
let signers = if pubkey.is_some() {
vec![]
} else {
vec![default_signer.signer_from_path(matches, wallet_manager)?]
};
let lamports = lamports_of_sol(matches, "amount").unwrap();
Ok(CliCommandInfo {
command: CliCommand::Airdrop { pubkey, lamports },
signers,
})
}
pub fn parse_balance(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let pubkey = pubkey_of_signer(matches, "pubkey", wallet_manager)?;
let signers = if pubkey.is_some() {
vec![]
} else {
vec![default_signer.signer_from_path(matches, wallet_manager)?]
};
Ok(CliCommandInfo {
command: CliCommand::Balance {
pubkey,
use_lamports_unit: matches.is_present("lamports"),
},
signers,
})
}
pub fn parse_decode_transaction(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
let blob = value_t_or_exit!(matches, "transaction", String);
let binary_encoding = match matches.value_of("encoding").unwrap() {
"base58" => TransactionBinaryEncoding::Base58,
"base64" => TransactionBinaryEncoding::Base64,
_ => unreachable!(),
};
let encoded_transaction = EncodedTransaction::Binary(blob, binary_encoding);
if let Some(transaction) = encoded_transaction.decode() {
Ok(CliCommandInfo {
command: CliCommand::DecodeTransaction(transaction),
signers: vec![],
})
} else {
Err(CliError::BadParameter(
"Unable to decode transaction".to_string(),
))
}
}
pub fn parse_create_address_with_seed(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let from_pubkey = pubkey_of_signer(matches, "from", wallet_manager)?;
let signers = if from_pubkey.is_some() {
vec![]
} else {
vec![default_signer.signer_from_path(matches, wallet_manager)?]
};
let program_id = resolve_derived_address_program_id(matches, "program_id").unwrap();
let seed = matches.value_of("seed").unwrap().to_string();
Ok(CliCommandInfo {
command: CliCommand::CreateAddressWithSeed {
from_pubkey,
seed,
program_id,
},
signers,
})
}
pub fn parse_find_program_derived_address(
matches: &ArgMatches<'_>,
) -> Result<CliCommandInfo, CliError> {
let program_id = resolve_derived_address_program_id(matches, "program_id")
.ok_or_else(|| CliError::BadParameter("PROGRAM_ID".to_string()))?;
let seeds = matches
.values_of("seeds")
.map(|seeds| {
seeds
.map(|value| {
let (prefix, value) = value.split_once(':').unwrap();
match prefix {
"pubkey" => Pubkey::from_str(value).unwrap().to_bytes().to_vec(),
"string" => value.as_bytes().to_vec(),
"hex" => Vec::<u8>::from_hex(value).unwrap(),
"u8" => u8::from_str(value).unwrap().to_le_bytes().to_vec(),
"u16le" => u16::from_str(value).unwrap().to_le_bytes().to_vec(),
"u32le" => u32::from_str(value).unwrap().to_le_bytes().to_vec(),
"u64le" => u64::from_str(value).unwrap().to_le_bytes().to_vec(),
"u128le" => u128::from_str(value).unwrap().to_le_bytes().to_vec(),
"i16le" => i16::from_str(value).unwrap().to_le_bytes().to_vec(),
"i32le" => i32::from_str(value).unwrap().to_le_bytes().to_vec(),
"i64le" => i64::from_str(value).unwrap().to_le_bytes().to_vec(),
"i128le" => i128::from_str(value).unwrap().to_le_bytes().to_vec(),
"u16be" => u16::from_str(value).unwrap().to_be_bytes().to_vec(),
"u32be" => u32::from_str(value).unwrap().to_be_bytes().to_vec(),
"u64be" => u64::from_str(value).unwrap().to_be_bytes().to_vec(),
"u128be" => u128::from_str(value).unwrap().to_be_bytes().to_vec(),
"i16be" => i16::from_str(value).unwrap().to_be_bytes().to_vec(),
"i32be" => i32::from_str(value).unwrap().to_be_bytes().to_vec(),
"i64be" => i64::from_str(value).unwrap().to_be_bytes().to_vec(),
"i128be" => i128::from_str(value).unwrap().to_be_bytes().to_vec(),
// Must be unreachable due to arg validator
_ => unreachable!("parse_find_program_derived_address: {prefix}:{value}"),
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
Ok(CliCommandInfo {
command: CliCommand::FindProgramDerivedAddress { seeds, program_id },
signers: vec![],
})
}
pub fn parse_transfer(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let amount = SpendAmount::new_from_matches(matches, "amount");
let to = pubkey_of_signer(matches, "to", wallet_manager)?.unwrap();
let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name);
let no_wait = matches.is_present("no_wait");
let blockhash_query = BlockhashQuery::new_from_matches(matches);
let nonce_account = pubkey_of_signer(matches, NONCE_ARG.name, wallet_manager)?;
let (nonce_authority, nonce_authority_pubkey) =
signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?;
let memo = matches.value_of(MEMO_ARG.name).map(String::from);
let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;
let (from, from_pubkey) = signer_of(matches, "from", wallet_manager)?;
let allow_unfunded_recipient = matches.is_present("allow_unfunded_recipient");
let mut bulk_signers = vec![fee_payer, from];
if nonce_account.is_some() {
bulk_signers.push(nonce_authority);
}
let signer_info =
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name);
let derived_address_seed = matches
.value_of("derived_address_seed")
.map(|s| s.to_string());
let derived_address_program_id =
resolve_derived_address_program_id(matches, "derived_address_program_id");
Ok(CliCommandInfo {
command: CliCommand::Transfer {
amount,
to,
sign_only,
dump_transaction_message,
allow_unfunded_recipient,
no_wait,
blockhash_query,
nonce_account,
nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(),
memo,
fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
from: signer_info.index_of(from_pubkey).unwrap(),
derived_address_seed,
derived_address_program_id,
compute_unit_price,
},
signers: signer_info.signers,
})
}
pub fn parse_sign_offchain_message(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let version: u8 = value_of(matches, "version").unwrap();
let message_text: String = value_of(matches, "message")
.ok_or_else(|| CliError::BadParameter("MESSAGE".to_string()))?;
let message = OffchainMessage::new(version, message_text.as_bytes())
.map_err(|_| CliError::BadParameter("VERSION or MESSAGE".to_string()))?;
Ok(CliCommandInfo {
command: CliCommand::SignOffchainMessage { message },
signers: vec![default_signer.signer_from_path(matches, wallet_manager)?],
})
}
pub fn parse_verify_offchain_signature(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let version: u8 = value_of(matches, "version").unwrap();
let message_text: String = value_of(matches, "message")
.ok_or_else(|| CliError::BadParameter("MESSAGE".to_string()))?;
let message = OffchainMessage::new(version, message_text.as_bytes())
.map_err(|_| CliError::BadParameter("VERSION or MESSAGE".to_string()))?;
let signer_pubkey = pubkey_of_signer(matches, "signer", wallet_manager)?;
let signers = if signer_pubkey.is_some() {
vec![]
} else {
vec![default_signer.signer_from_path(matches, wallet_manager)?]
};
let signature = value_of(matches, "signature")
.ok_or_else(|| CliError::BadParameter("SIGNATURE".to_string()))?;
Ok(CliCommandInfo {
command: CliCommand::VerifyOffchainSignature {
signer_pubkey,
signature,
message,
},
signers,
})
}
pub fn process_show_account(
rpc_client: &RpcClient,
config: &CliConfig,
account_pubkey: &Pubkey,
output_file: &Option<String>,
use_lamports_unit: bool,
) -> ProcessResult {
let account = rpc_client.get_account(account_pubkey)?;
let data = account.data.clone();
let cli_account = CliAccount::new(account_pubkey, &account, use_lamports_unit);
let mut account_string = config.output_format.formatted_string(&cli_account);
match config.output_format {
OutputFormat::Json | OutputFormat::JsonCompact => {
if let Some(output_file) = output_file {
let mut f = File::create(output_file)?;
f.write_all(account_string.as_bytes())?;
writeln!(&mut account_string)?;
writeln!(&mut account_string, "Wrote account to {output_file}")?;
}
}
OutputFormat::Display | OutputFormat::DisplayVerbose => {
if let Some(output_file) = output_file {
let mut f = File::create(output_file)?;
f.write_all(&data)?;
writeln!(&mut account_string)?;
writeln!(&mut account_string, "Wrote account data to {output_file}")?;
} else if !data.is_empty() {
use pretty_hex::*;
writeln!(&mut account_string, "{:?}", data.hex_dump())?;
}
}
OutputFormat::DisplayQuiet => (),
}
Ok(account_string)
}
pub fn process_airdrop(
rpc_client: &RpcClient,
config: &CliConfig,
pubkey: &Option<Pubkey>,
lamports: u64,
) -> ProcessResult {
let pubkey = if let Some(pubkey) = pubkey {
*pubkey
} else {
config.pubkey()?
};
println!(
"Requesting airdrop of {}",
build_balance_message(lamports, false, true),
);
let pre_balance = rpc_client.get_balance(&pubkey)?;
let result = request_and_confirm_airdrop(rpc_client, config, &pubkey, lamports);
if let Ok(signature) = result {
let signature_cli_message = log_instruction_custom_error::<SystemError>(result, config)?;
println!("{signature_cli_message}");
let current_balance = rpc_client.get_balance(&pubkey)?;
if current_balance < pre_balance.saturating_add(lamports) {
println!("Balance unchanged");
println!("Run `solana confirm -v {signature:?}` for more info");
Ok("".to_string())
} else {
Ok(build_balance_message(current_balance, false, true))
}
} else {
log_instruction_custom_error::<SystemError>(result, config)
}
}
pub fn process_balance(
rpc_client: &RpcClient,
config: &CliConfig,
pubkey: &Option<Pubkey>,
use_lamports_unit: bool,
) -> ProcessResult {
let pubkey = if let Some(pubkey) = pubkey {
*pubkey
} else {
config.pubkey()?
};
let balance = rpc_client.get_balance(&pubkey)?;
let balance_output = CliBalance {
lamports: balance,
config: BuildBalanceMessageConfig {
use_lamports_unit,
show_unit: true,
trim_trailing_zeros: true,
},
};
Ok(config.output_format.formatted_string(&balance_output))
}
pub fn process_confirm(
rpc_client: &RpcClient,
config: &CliConfig,
signature: &Signature,
) -> ProcessResult {
match rpc_client.get_signature_statuses_with_history(&[*signature]) {
Ok(status) => {
let cli_transaction = if let Some(transaction_status) = &status.value[0] {
let mut transaction = None;
let mut get_transaction_error = None;
if config.verbose {
match rpc_client.get_transaction_with_config(
signature,
RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::Base64),
commitment: Some(CommitmentConfig::confirmed()),
max_supported_transaction_version: Some(0),
},
) {
Ok(confirmed_transaction) => {
let EncodedConfirmedTransactionWithStatusMeta {
block_time,
slot,
transaction: transaction_with_meta,
} = confirmed_transaction;
let decoded_transaction =
transaction_with_meta.transaction.decode().unwrap();
let json_transaction = decoded_transaction.json_encode();
transaction = Some(CliTransaction {
transaction: json_transaction,
meta: transaction_with_meta.meta,
block_time,
slot: Some(slot),
decoded_transaction,
prefix: " ".to_string(),
sigverify_status: vec![],
});
}
Err(err) => {
get_transaction_error = Some(format!("{err:?}"));
}
}
}
CliTransactionConfirmation {
confirmation_status: Some(transaction_status.confirmation_status()),
transaction,
get_transaction_error,
err: transaction_status.err.clone(),
}
} else {
CliTransactionConfirmation {
confirmation_status: None,
transaction: None,
get_transaction_error: None,
err: None,
}
};
Ok(config.output_format.formatted_string(&cli_transaction))
}
Err(err) => Err(CliError::RpcRequestError(format!("Unable to confirm: {err}")).into()),
}
}
#[allow(clippy::unnecessary_wraps)]
pub fn process_decode_transaction(
config: &CliConfig,
transaction: &VersionedTransaction,
) -> ProcessResult {
let sigverify_status = CliSignatureVerificationStatus::verify_transaction(transaction);
let decode_transaction = CliTransaction {
decoded_transaction: transaction.clone(),
transaction: transaction.json_encode(),
meta: None,
block_time: None,
slot: None,
prefix: "".to_string(),
sigverify_status,
};
Ok(config.output_format.formatted_string(&decode_transaction))
}
pub fn process_create_address_with_seed(
config: &CliConfig,
from_pubkey: Option<&Pubkey>,
seed: &str,
program_id: &Pubkey,
) -> ProcessResult {
let from_pubkey = if let Some(pubkey) = from_pubkey {
*pubkey
} else {
config.pubkey()?
};
let address = Pubkey::create_with_seed(&from_pubkey, seed, program_id)?;
Ok(address.to_string())
}
pub fn process_find_program_derived_address(
config: &CliConfig,
seeds: &Vec<Vec<u8>>,
program_id: &Pubkey,
) -> ProcessResult {
if config.verbose {
println!("Seeds: {seeds:?}");
}
let seeds_slice = seeds.iter().map(|x| &x[..]).collect::<Vec<_>>();
let (address, bump_seed) = Pubkey::find_program_address(&seeds_slice[..], program_id);
let result = CliFindProgramDerivedAddress {
address: address.to_string(),
bump_seed,
};
Ok(config.output_format.formatted_string(&result))
}
#[allow(clippy::too_many_arguments)]
pub fn process_transfer(
rpc_client: &RpcClient,
config: &CliConfig,
amount: SpendAmount,
to: &Pubkey,
from: SignerIndex,
sign_only: bool,
dump_transaction_message: bool,
allow_unfunded_recipient: bool,
no_wait: bool,
blockhash_query: &BlockhashQuery,
nonce_account: Option<&Pubkey>,
nonce_authority: SignerIndex,
memo: Option<&String>,
fee_payer: SignerIndex,
derived_address_seed: Option<String>,
derived_address_program_id: Option<&Pubkey>,
compute_unit_price: Option<&u64>,
) -> ProcessResult {
let from = config.signers[from];
let mut from_pubkey = from.pubkey();
let recent_blockhash = blockhash_query.get_blockhash(rpc_client, config.commitment)?;
if !sign_only && !allow_unfunded_recipient {
let recipient_balance = rpc_client
.get_balance_with_commitment(to, config.commitment)?
.value;
if recipient_balance == 0 {
return Err(format!(
"The recipient address ({to}) is not funded. \
Add `--allow-unfunded-recipient` to complete the transfer \
"
)
.into());
}
}
let nonce_authority = config.signers[nonce_authority];
let fee_payer = config.signers[fee_payer];
let derived_parts = derived_address_seed.zip(derived_address_program_id);
let with_seed = if let Some((seed, program_id)) = derived_parts {
let base_pubkey = from_pubkey;
from_pubkey = Pubkey::create_with_seed(&base_pubkey, &seed, program_id)?;
Some((base_pubkey, seed, program_id, from_pubkey))
} else {
None
};
let build_message = |lamports| {
let ixs = if let Some((base_pubkey, seed, program_id, from_pubkey)) = with_seed.as_ref() {
vec![system_instruction::transfer_with_seed(
from_pubkey,
base_pubkey,
seed.clone(),
program_id,
to,
lamports,
)]
.with_memo(memo)
.with_compute_unit_price(compute_unit_price)
} else {
vec![system_instruction::transfer(&from_pubkey, to, lamports)]
.with_memo(memo)
.with_compute_unit_price(compute_unit_price)
};
if let Some(nonce_account) = &nonce_account {
Message::new_with_nonce(
ixs,
Some(&fee_payer.pubkey()),
nonce_account,
&nonce_authority.pubkey(),
)
} else {
Message::new(&ixs, Some(&fee_payer.pubkey()))
}
};
let (message, _) = resolve_spend_tx_and_check_account_balances(
rpc_client,
sign_only,
amount,
&recent_blockhash,
&from_pubkey,
&fee_payer.pubkey(),
build_message,
config.commitment,
)?;
let mut tx = Transaction::new_unsigned(message);
if sign_only {
tx.try_partial_sign(&config.signers, recent_blockhash)?;
return_signers_with_config(
&tx,
&config.output_format,
&ReturnSignersConfig {
dump_transaction_message,
},
)
} else {
if let Some(nonce_account) = &nonce_account {
let nonce_account = solana_rpc_client_nonce_utils::get_account_with_commitment(
rpc_client,
nonce_account,
config.commitment,
)?;
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
}
tx.try_sign(&config.signers, recent_blockhash)?;
let result = if no_wait {
rpc_client.send_transaction(&tx)
} else {
rpc_client.send_and_confirm_transaction_with_spinner(&tx)
};
log_instruction_custom_error::<SystemError>(result, config)
}
}
pub fn process_sign_offchain_message(
config: &CliConfig,
message: &OffchainMessage,
) -> ProcessResult {
Ok(message.sign(config.signers[0])?.to_string())
}
pub fn process_verify_offchain_signature(
config: &CliConfig,
signer_pubkey: &Option<Pubkey>,
signature: &Signature,
message: &OffchainMessage,
) -> ProcessResult {
let signer = if let Some(pubkey) = signer_pubkey {
*pubkey
} else {
config.signers[0].pubkey()
};
if message.verify(&signer, signature)? {
Ok("Signature is valid".to_string())
} else {
Err(CliError::InvalidSignature.into())
}
}