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
This commit is contained in:
DimAn 2023-04-17 09:17:40 +02:00 committed by GitHub
parent f6259fa4b4
commit 7d556a110d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 222 additions and 4 deletions

1
Cargo.lock generated
View File

@ -5279,6 +5279,7 @@ dependencies = [
"criterion-stats",
"crossbeam-channel",
"ctrlc",
"hex",
"humantime",
"log",
"num-traits",

View File

@ -334,6 +334,51 @@ where
.map(|_| ())
}
pub fn is_structured_seed<T>(value: T) -> Result<(), String>
where
T: AsRef<str> + 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<T>(value: T) -> Result<(), String>
where
T: AsRef<str> + Display,

View File

@ -328,6 +328,51 @@ where
.map(|_| ())
}
pub fn is_structured_seed<T>(value: T) -> Result<(), String>
where
T: AsRef<str> + 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<T>(value: T) -> Result<(), String>
where
T: AsRef<str> + Display,

View File

@ -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 {

View File

@ -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 }

View File

@ -59,6 +59,10 @@ pub enum CliCommand {
Fees {
blockhash: Option<Hash>,
},
FindProgramDerivedAddress {
seeds: Vec<Vec<u8>>,
program_id: Pubkey,
},
FirstAvailableBlock,
GetBlock {
slot: Option<Slot>,
@ -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),

View File

@ -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<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,
@ -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<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,