From 7d556a110d8f1d5640b817cfc96dcc56a4de46e0 Mon Sep 17 00:00:00 2001 From: DimAn Date: Mon, 17 Apr 2023 09:17:40 +0200 Subject: [PATCH] Cli: add `find-program-derived-address` command (#30370) * Add find-program-address solana cli command * clippy * clippy after rebase * rename find-program-address -> find-program-derived-address * rename is_complex_seed -> is_structured_seed * add validator is_structured_seed to clap-v3-utils * return CliError::BadParameter for PROGRAM_ID arg in case of incorrect parsing * improve help for SEEDS arg * extend About for create-address-with-seed command * fix SEED help --- Cargo.lock | 1 + clap-utils/src/input_validators.rs | 45 +++++++++++ clap-v3-utils/src/input_validators.rs | 45 +++++++++++ cli-output/src/cli_output.rs | 17 ++++ cli/Cargo.toml | 1 + cli/src/cli.rs | 10 +++ cli/src/wallet.rs | 107 +++++++++++++++++++++++++- 7 files changed, 222 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfa0965060..fe7aa621bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5279,6 +5279,7 @@ dependencies = [ "criterion-stats", "crossbeam-channel", "ctrlc", + "hex", "humantime", "log", "num-traits", diff --git a/clap-utils/src/input_validators.rs b/clap-utils/src/input_validators.rs index ecd7cf2e7d..ff86e1f10c 100644 --- a/clap-utils/src/input_validators.rs +++ b/clap-utils/src/input_validators.rs @@ -334,6 +334,51 @@ where .map(|_| ()) } +pub fn is_structured_seed(value: T) -> Result<(), String> +where + T: AsRef + Display, +{ + let (prefix, value) = value + .as_ref() + .split_once(':') + .ok_or("Seed must contain ':' as delimiter") + .unwrap(); + if prefix.is_empty() || value.is_empty() { + Err(String::from("Seed prefix or value is empty")) + } else { + match prefix { + "string" | "pubkey" | "hex" | "u8" => Ok(()), + _ => { + let len = prefix.len(); + if len != 5 && len != 6 { + Err(format!("Wrong prefix length {len} {prefix}:{value}")) + } else { + let sign = &prefix[0..1]; + let type_size = &prefix[1..len.saturating_sub(2)]; + let byte_order = &prefix[len.saturating_sub(2)..len]; + if sign != "u" && sign != "i" { + Err(format!("Wrong prefix sign {sign} {prefix}:{value}")) + } else if type_size != "16" + && type_size != "32" + && type_size != "64" + && type_size != "128" + { + Err(format!( + "Wrong prefix type size {type_size} {prefix}:{value}" + )) + } else if byte_order != "le" && byte_order != "be" { + Err(format!( + "Wrong prefix byte order {byte_order} {prefix}:{value}" + )) + } else { + Ok(()) + } + } + } + } + } +} + pub fn is_derived_address_seed(value: T) -> Result<(), String> where T: AsRef + Display, diff --git a/clap-v3-utils/src/input_validators.rs b/clap-v3-utils/src/input_validators.rs index cd016a209f..33888a9149 100644 --- a/clap-v3-utils/src/input_validators.rs +++ b/clap-v3-utils/src/input_validators.rs @@ -328,6 +328,51 @@ where .map(|_| ()) } +pub fn is_structured_seed(value: T) -> Result<(), String> +where + T: AsRef + Display, +{ + let (prefix, value) = value + .as_ref() + .split_once(':') + .ok_or("Seed must contain ':' as delimiter") + .unwrap(); + if prefix.is_empty() || value.is_empty() { + Err(String::from("Seed prefix or value is empty")) + } else { + match prefix { + "string" | "pubkey" | "hex" | "u8" => Ok(()), + _ => { + let len = prefix.len(); + if len != 5 && len != 6 { + Err(format!("Wrong prefix length {len} {prefix}:{value}")) + } else { + let sign = &prefix[0..1]; + let type_size = &prefix[1..len.saturating_sub(2)]; + let byte_order = &prefix[len.saturating_sub(2)..len]; + if sign != "u" && sign != "i" { + Err(format!("Wrong prefix sign {sign} {prefix}:{value}")) + } else if type_size != "16" + && type_size != "32" + && type_size != "64" + && type_size != "128" + { + Err(format!( + "Wrong prefix type size {type_size} {prefix}:{value}" + )) + } else if byte_order != "le" && byte_order != "be" { + Err(format!( + "Wrong prefix byte order {byte_order} {prefix}:{value}" + )) + } else { + Ok(()) + } + } + } + } + } +} + pub fn is_derived_address_seed(value: T) -> Result<(), String> where T: AsRef + Display, diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 9dd45a0f79..cf211c3889 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -2986,6 +2986,23 @@ impl fmt::Display for CliBalance { } } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliFindProgramDerivedAddress { + pub address: String, + pub bump_seed: u8, +} + +impl QuietDisplay for CliFindProgramDerivedAddress {} +impl VerboseDisplay for CliFindProgramDerivedAddress {} + +impl fmt::Display for CliFindProgramDerivedAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.address)?; + Ok(()) + } +} + #[cfg(test)] mod tests { use { diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 50aa3f67bf..fe19b6000d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,6 +18,7 @@ const_format = { workspace = true } criterion-stats = { workspace = true } crossbeam-channel = { workspace = true } ctrlc = { workspace = true, features = ["termination"] } +hex = { workspace = true } humantime = { workspace = true } log = { workspace = true } num-traits = { workspace = true } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 07f31242dd..91f39214a7 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -59,6 +59,10 @@ pub enum CliCommand { Fees { blockhash: Option, }, + FindProgramDerivedAddress { + seeds: Vec>, + program_id: Pubkey, + }, FirstAvailableBlock, GetBlock { slot: Option, @@ -802,6 +806,9 @@ pub fn parse_command( ("create-address-with-seed", Some(matches)) => { parse_create_address_with_seed(matches, default_signer, wallet_manager) } + ("find-program-derived-address", Some(matches)) => { + parse_find_program_derived_address(matches) + } ("decode-transaction", Some(matches)) => parse_decode_transaction(matches), ("resolve-signer", Some(matches)) => { let signer_path = resolve_signer(matches, "signer", wallet_manager)?; @@ -888,6 +895,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::Feature(feature_subcommand) => { process_feature_subcommand(&rpc_client, config, feature_subcommand) } + CliCommand::FindProgramDerivedAddress { seeds, program_id } => { + process_find_program_derived_address(config, seeds, program_id) + } CliCommand::FirstAvailableBlock => process_first_available_block(&rpc_client), CliCommand::GetBlock { slot } => process_get_block(&rpc_client, config, *slot), CliCommand::GetBlockTime { slot } => process_get_block_time(&rpc_client, config, *slot), diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index df21038a0a..27a6b55761 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -10,6 +10,7 @@ use { 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::*, @@ -23,8 +24,9 @@ use { }, solana_cli_output::{ display::{build_balance_message, BuildBalanceMessageConfig}, - return_signers_with_config, CliAccount, CliBalance, CliSignatureVerificationStatus, - CliTransaction, CliTransactionConfirmation, OutputFormat, ReturnSignersConfig, + return_signers_with_config, CliAccount, CliBalance, CliFindProgramDerivedAddress, + CliSignatureVerificationStatus, CliTransaction, CliTransactionConfirmation, OutputFormat, + ReturnSignersConfig, }, solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_rpc_client::rpc_client::RpcClient, @@ -45,7 +47,7 @@ use { EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, TransactionBinaryEncoding, UiTransactionEncoding, }, - std::{fmt::Write as FmtWrite, fs::File, io::Write, sync::Arc}, + std::{fmt::Write as FmtWrite, fs::File, io::Write, str::FromStr, sync::Arc}, }; pub trait WalletSubCommands { @@ -150,7 +152,10 @@ impl WalletSubCommands for App<'_, '_> { ) .subcommand( SubCommand::with_name("create-address-with-seed") - .about("Generate a derived account address with a 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) @@ -179,6 +184,37 @@ impl WalletSubCommands for App<'_, '_> { "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") @@ -457,6 +493,52 @@ pub fn parse_create_address_with_seed( }) } +pub fn parse_find_program_derived_address( + matches: &ArgMatches<'_>, +) -> Result { + 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::::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::>() + }) + .unwrap_or_default(); + + Ok(CliCommandInfo { + command: CliCommand::FindProgramDerivedAddress { seeds, program_id }, + signers: vec![], + }) +} + pub fn parse_transfer( matches: &ArgMatches<'_>, default_signer: &DefaultSigner, @@ -759,6 +841,23 @@ pub fn process_create_address_with_seed( Ok(address.to_string()) } +pub fn process_find_program_derived_address( + config: &CliConfig, + seeds: &Vec>, + program_id: &Pubkey, +) -> ProcessResult { + if config.verbose { + println!("Seeds: {seeds:?}"); + } + let seeds_slice = seeds.iter().map(|x| &x[..]).collect::>(); + 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,