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:
parent
f6259fa4b4
commit
7d556a110d
|
@ -5279,6 +5279,7 @@ dependencies = [
|
|||
"criterion-stats",
|
||||
"crossbeam-channel",
|
||||
"ctrlc",
|
||||
"hex",
|
||||
"humantime",
|
||||
"log",
|
||||
"num-traits",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue