Skip to content

Commit 7f53788

Browse files
diman-ioilya-bobyr
authored andcommitted
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
1 parent 8f8cab9 commit 7f53788

File tree

7 files changed

+222
-4
lines changed

7 files changed

+222
-4
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clap-utils/src/input_validators.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,51 @@ where
354354
.map(|_| ())
355355
}
356356

357+
pub fn is_structured_seed<T>(value: T) -> Result<(), String>
358+
where
359+
T: AsRef<str> + Display,
360+
{
361+
let (prefix, value) = value
362+
.as_ref()
363+
.split_once(':')
364+
.ok_or("Seed must contain ':' as delimiter")
365+
.unwrap();
366+
if prefix.is_empty() || value.is_empty() {
367+
Err(String::from("Seed prefix or value is empty"))
368+
} else {
369+
match prefix {
370+
"string" | "pubkey" | "hex" | "u8" => Ok(()),
371+
_ => {
372+
let len = prefix.len();
373+
if len != 5 && len != 6 {
374+
Err(format!("Wrong prefix length {len} {prefix}:{value}"))
375+
} else {
376+
let sign = &prefix[0..1];
377+
let type_size = &prefix[1..len.saturating_sub(2)];
378+
let byte_order = &prefix[len.saturating_sub(2)..len];
379+
if sign != "u" && sign != "i" {
380+
Err(format!("Wrong prefix sign {sign} {prefix}:{value}"))
381+
} else if type_size != "16"
382+
&& type_size != "32"
383+
&& type_size != "64"
384+
&& type_size != "128"
385+
{
386+
Err(format!(
387+
"Wrong prefix type size {type_size} {prefix}:{value}"
388+
))
389+
} else if byte_order != "le" && byte_order != "be" {
390+
Err(format!(
391+
"Wrong prefix byte order {byte_order} {prefix}:{value}"
392+
))
393+
} else {
394+
Ok(())
395+
}
396+
}
397+
}
398+
}
399+
}
400+
}
401+
357402
pub fn is_derived_address_seed<T>(value: T) -> Result<(), String>
358403
where
359404
T: AsRef<str> + Display,

clap-v3-utils/src/input_validators.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,51 @@ where
348348
.map(|_| ())
349349
}
350350

351+
pub fn is_structured_seed<T>(value: T) -> Result<(), String>
352+
where
353+
T: AsRef<str> + Display,
354+
{
355+
let (prefix, value) = value
356+
.as_ref()
357+
.split_once(':')
358+
.ok_or("Seed must contain ':' as delimiter")
359+
.unwrap();
360+
if prefix.is_empty() || value.is_empty() {
361+
Err(String::from("Seed prefix or value is empty"))
362+
} else {
363+
match prefix {
364+
"string" | "pubkey" | "hex" | "u8" => Ok(()),
365+
_ => {
366+
let len = prefix.len();
367+
if len != 5 && len != 6 {
368+
Err(format!("Wrong prefix length {len} {prefix}:{value}"))
369+
} else {
370+
let sign = &prefix[0..1];
371+
let type_size = &prefix[1..len.saturating_sub(2)];
372+
let byte_order = &prefix[len.saturating_sub(2)..len];
373+
if sign != "u" && sign != "i" {
374+
Err(format!("Wrong prefix sign {sign} {prefix}:{value}"))
375+
} else if type_size != "16"
376+
&& type_size != "32"
377+
&& type_size != "64"
378+
&& type_size != "128"
379+
{
380+
Err(format!(
381+
"Wrong prefix type size {type_size} {prefix}:{value}"
382+
))
383+
} else if byte_order != "le" && byte_order != "be" {
384+
Err(format!(
385+
"Wrong prefix byte order {byte_order} {prefix}:{value}"
386+
))
387+
} else {
388+
Ok(())
389+
}
390+
}
391+
}
392+
}
393+
}
394+
}
395+
351396
pub fn is_derived_address_seed<T>(value: T) -> Result<(), String>
352397
where
353398
T: AsRef<str> + Display,

cli-output/src/cli_output.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2821,6 +2821,23 @@ impl fmt::Display for CliBalance {
28212821
}
28222822
}
28232823

2824+
#[derive(Serialize, Deserialize)]
2825+
#[serde(rename_all = "camelCase")]
2826+
pub struct CliFindProgramDerivedAddress {
2827+
pub address: String,
2828+
pub bump_seed: u8,
2829+
}
2830+
2831+
impl QuietDisplay for CliFindProgramDerivedAddress {}
2832+
impl VerboseDisplay for CliFindProgramDerivedAddress {}
2833+
2834+
impl fmt::Display for CliFindProgramDerivedAddress {
2835+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2836+
write!(f, "{}", self.address)?;
2837+
Ok(())
2838+
}
2839+
}
2840+
28242841
#[cfg(test)]
28252842
mod tests {
28262843
use {

cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const_format = "0.2.25"
1818
criterion-stats = "0.3.0"
1919
crossbeam-channel = "0.5"
2020
ctrlc = { version = "3.2.2", features = ["termination"] }
21+
hex = "0.4.3"
2122
humantime = "2.0.1"
2223
log = "0.4.17"
2324
num-traits = "0.2"

cli/src/cli.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ pub enum CliCommand {
6060
Fees {
6161
blockhash: Option<Hash>,
6262
},
63+
FindProgramDerivedAddress {
64+
seeds: Vec<Vec<u8>>,
65+
program_id: Pubkey,
66+
},
6367
FirstAvailableBlock,
6468
GetBlock {
6569
slot: Option<Slot>,
@@ -810,6 +814,9 @@ pub fn parse_command(
810814
("create-address-with-seed", Some(matches)) => {
811815
parse_create_address_with_seed(matches, default_signer, wallet_manager)
812816
}
817+
("find-program-derived-address", Some(matches)) => {
818+
parse_find_program_derived_address(matches)
819+
}
813820
("decode-transaction", Some(matches)) => parse_decode_transaction(matches),
814821
("resolve-signer", Some(matches)) => {
815822
let signer_path = resolve_signer(matches, "signer", wallet_manager)?;
@@ -890,6 +897,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
890897
CliCommand::Feature(feature_subcommand) => {
891898
process_feature_subcommand(&rpc_client, config, feature_subcommand)
892899
}
900+
CliCommand::FindProgramDerivedAddress { seeds, program_id } => {
901+
process_find_program_derived_address(config, seeds, program_id)
902+
}
893903
CliCommand::FirstAvailableBlock => process_first_available_block(&rpc_client),
894904
CliCommand::GetBlock { slot } => process_get_block(&rpc_client, config, *slot),
895905
CliCommand::GetBlockTime { slot } => process_get_block_time(&rpc_client, config, *slot),

cli/src/wallet.rs

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use {
1010
spend_utils::{resolve_spend_tx_and_check_account_balances, SpendAmount},
1111
},
1212
clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand},
13+
hex::FromHex,
1314
solana_account_decoder::{UiAccount, UiAccountEncoding},
1415
solana_clap_utils::{
1516
compute_unit_price::{compute_unit_price_arg, COMPUTE_UNIT_PRICE_ARG},
@@ -23,8 +24,9 @@ use {
2324
},
2425
solana_cli_output::{
2526
display::{build_balance_message, BuildBalanceMessageConfig},
26-
return_signers_with_config, CliAccount, CliBalance, CliSignatureVerificationStatus,
27-
CliTransaction, CliTransactionConfirmation, OutputFormat, ReturnSignersConfig,
27+
return_signers_with_config, CliAccount, CliBalance, CliFindProgramDerivedAddress,
28+
CliSignatureVerificationStatus, CliTransaction, CliTransactionConfirmation, OutputFormat,
29+
ReturnSignersConfig,
2830
},
2931
solana_client::{
3032
blockhash_query::BlockhashQuery, nonce_utils, rpc_client::RpcClient,
@@ -45,7 +47,7 @@ use {
4547
EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
4648
TransactionBinaryEncoding, UiTransactionEncoding,
4749
},
48-
std::{fmt::Write as FmtWrite, fs::File, io::Write, sync::Arc},
50+
std::{fmt::Write as FmtWrite, fs::File, io::Write, str::FromStr, sync::Arc},
4951
};
5052

5153
pub trait WalletSubCommands {
@@ -150,7 +152,10 @@ impl WalletSubCommands for App<'_, '_> {
150152
)
151153
.subcommand(
152154
SubCommand::with_name("create-address-with-seed")
153-
.about("Generate a derived account address with a seed")
155+
.about(
156+
"Generate a derived account address with a seed. \
157+
For program derived addresses (PDAs), use the find-program-derived-address command instead"
158+
)
154159
.arg(
155160
Arg::with_name("seed")
156161
.index(1)
@@ -179,6 +184,37 @@ impl WalletSubCommands for App<'_, '_> {
179184
"From (base) key, [default: cli config keypair]. "),
180185
),
181186
)
187+
.subcommand(
188+
SubCommand::with_name("find-program-derived-address")
189+
.about("Generate a program derived account address with a seed")
190+
.arg(
191+
Arg::with_name("program_id")
192+
.index(1)
193+
.value_name("PROGRAM_ID")
194+
.takes_value(true)
195+
.required(true)
196+
.help(
197+
"The program_id that the address will ultimately be used for, \n\
198+
or one of NONCE, STAKE, and VOTE keywords",
199+
),
200+
)
201+
.arg(
202+
Arg::with_name("seeds")
203+
.min_values(0)
204+
.value_name("SEED")
205+
.takes_value(true)
206+
.validator(is_structured_seed)
207+
.help(
208+
"The seeds. \n\
209+
Each one must match the pattern PREFIX:VALUE. \n\
210+
PREFIX can be one of [string, pubkey, hex, u8] \n\
211+
or matches the pattern [u,i][16,32,64,128][le,be] (for example u64le) for number values \n\
212+
[u,i] - represents whether the number is unsigned or signed, \n\
213+
[16,32,64,128] - represents the bit length, and \n\
214+
[le,be] - represents the byte order - little endian or big endian"
215+
),
216+
),
217+
)
182218
.subcommand(
183219
SubCommand::with_name("decode-transaction")
184220
.about("Decode a serialized transaction")
@@ -392,6 +428,52 @@ pub fn parse_create_address_with_seed(
392428
})
393429
}
394430

431+
pub fn parse_find_program_derived_address(
432+
matches: &ArgMatches<'_>,
433+
) -> Result<CliCommandInfo, CliError> {
434+
let program_id = resolve_derived_address_program_id(matches, "program_id")
435+
.ok_or_else(|| CliError::BadParameter("PROGRAM_ID".to_string()))?;
436+
let seeds = matches
437+
.values_of("seeds")
438+
.map(|seeds| {
439+
seeds
440+
.map(|value| {
441+
let (prefix, value) = value.split_once(':').unwrap();
442+
match prefix {
443+
"pubkey" => Pubkey::from_str(value).unwrap().to_bytes().to_vec(),
444+
"string" => value.as_bytes().to_vec(),
445+
"hex" => Vec::<u8>::from_hex(value).unwrap(),
446+
"u8" => u8::from_str(value).unwrap().to_le_bytes().to_vec(),
447+
"u16le" => u16::from_str(value).unwrap().to_le_bytes().to_vec(),
448+
"u32le" => u32::from_str(value).unwrap().to_le_bytes().to_vec(),
449+
"u64le" => u64::from_str(value).unwrap().to_le_bytes().to_vec(),
450+
"u128le" => u128::from_str(value).unwrap().to_le_bytes().to_vec(),
451+
"i16le" => i16::from_str(value).unwrap().to_le_bytes().to_vec(),
452+
"i32le" => i32::from_str(value).unwrap().to_le_bytes().to_vec(),
453+
"i64le" => i64::from_str(value).unwrap().to_le_bytes().to_vec(),
454+
"i128le" => i128::from_str(value).unwrap().to_le_bytes().to_vec(),
455+
"u16be" => u16::from_str(value).unwrap().to_be_bytes().to_vec(),
456+
"u32be" => u32::from_str(value).unwrap().to_be_bytes().to_vec(),
457+
"u64be" => u64::from_str(value).unwrap().to_be_bytes().to_vec(),
458+
"u128be" => u128::from_str(value).unwrap().to_be_bytes().to_vec(),
459+
"i16be" => i16::from_str(value).unwrap().to_be_bytes().to_vec(),
460+
"i32be" => i32::from_str(value).unwrap().to_be_bytes().to_vec(),
461+
"i64be" => i64::from_str(value).unwrap().to_be_bytes().to_vec(),
462+
"i128be" => i128::from_str(value).unwrap().to_be_bytes().to_vec(),
463+
// Must be unreachable due to arg validator
464+
_ => unreachable!("parse_find_program_derived_address: {prefix}:{value}"),
465+
}
466+
})
467+
.collect::<Vec<_>>()
468+
})
469+
.unwrap_or_default();
470+
471+
Ok(CliCommandInfo {
472+
command: CliCommand::FindProgramDerivedAddress { seeds, program_id },
473+
signers: vec![],
474+
})
475+
}
476+
395477
pub fn parse_transfer(
396478
matches: &ArgMatches<'_>,
397479
default_signer: &DefaultSigner,
@@ -658,6 +740,23 @@ pub fn process_create_address_with_seed(
658740
Ok(address.to_string())
659741
}
660742

743+
pub fn process_find_program_derived_address(
744+
config: &CliConfig,
745+
seeds: &Vec<Vec<u8>>,
746+
program_id: &Pubkey,
747+
) -> ProcessResult {
748+
if config.verbose {
749+
println!("Seeds: {seeds:?}");
750+
}
751+
let seeds_slice = seeds.iter().map(|x| &x[..]).collect::<Vec<_>>();
752+
let (address, bump_seed) = Pubkey::find_program_address(&seeds_slice[..], program_id);
753+
let result = CliFindProgramDerivedAddress {
754+
address: address.to_string(),
755+
bump_seed,
756+
};
757+
Ok(config.output_format.formatted_string(&result))
758+
}
759+
661760
#[allow(clippy::too_many_arguments)]
662761
pub fn process_transfer(
663762
rpc_client: &RpcClient,

0 commit comments

Comments
 (0)