From 9445a0e63fca76cfbaa198e948edba7590b48166 Mon Sep 17 00:00:00 2001 From: leovct Date: Mon, 1 Dec 2025 06:04:16 +0000 Subject: [PATCH 1/5] feat(cast): derive accounts from mnemonic --- crates/cast/src/cmd/wallet/mod.rs | 70 ++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index d7f7ffefe9d5f..a90d309acd7e3 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -87,6 +87,22 @@ pub enum WalletSubcommands { wallet: WalletOpts, }, + /// Derive accounts from a mnemonic + #[command(visible_alias = "d")] + Derive { + /// The accounts will be derived from the specified mnemonic phrase. + #[arg(value_name = "MNEMONIC")] + mnemonic: String, + + /// Number of accounts to display. + #[arg(long, short, default_value = "1")] + accounts: Option, + + /// Insecure mode: display private keys in the terminal. + #[arg(long, default_value = "false")] + insecure: bool, + }, + /// Sign a message or typed data. #[command(visible_alias = "s")] Sign { @@ -228,7 +244,7 @@ pub enum WalletSubcommands { /// Derives private key from mnemonic #[command(name = "private-key", visible_alias = "pk", aliases = &["derive-private-key", "--derive-private-key"])] PrivateKey { - /// If provided, the private key will be derived from the specified menomonic phrase. + /// If provided, the private key will be derived from the specified mnemonic phrase. #[arg(value_name = "MNEMONIC")] mnemonic_override: Option, @@ -491,6 +507,58 @@ impl WalletSubcommands { let addr = wallet.address(); sh_println!("{}", addr.to_checksum(None))?; } + Self::Derive {mnemonic, accounts, insecure} => { + let format_json = shell::is_json(); + let mut accounts_json = json!([]); + for i in 0..accounts.unwrap_or(1) { + let wallet = WalletOpts { + raw: RawWalletOpts { + mnemonic: Some(mnemonic.clone()), + mnemonic_index: i as u32, + ..Default::default() + }, + ..Default::default() + } + .signer() + .await?; + + match wallet { + WalletSigner::Local(local_wallet) => { + let address = local_wallet.address().to_checksum(None); + let private_key = hex::encode(local_wallet.credential().to_bytes()); + if format_json { + if insecure { + accounts_json.as_array_mut().unwrap().push( + json!({ + "address": format!("{}", address), + "private_key": format!("0x{}", private_key), + }) + ); + } else { + accounts_json.as_array_mut().unwrap().push( + json!({ + "address": format!("{}", address) + }) + ); + } + } else { + sh_println!("- Account {i}:")?; + sh_println!("Address: {}", address)?; + if insecure { + sh_println!("Private key: 0x{}", private_key)?; + } + } + } + _ => { + eyre::bail!("Expected local wallet for mnemonic derivation"); + } + } + } + + if format_json { + sh_println!("{}", serde_json::to_string_pretty(&accounts_json)?)?; + } + } Self::PublicKey { wallet, private_key_override } => { let wallet = private_key_override .map(|pk| WalletOpts { From 0fc05d4e56b122714f6462e3e6c1c56430fd953b Mon Sep 17 00:00:00 2001 From: leovct Date: Mon, 1 Dec 2025 06:11:18 +0000 Subject: [PATCH 2/5] chore: cargo fmt --- crates/cast/src/cmd/wallet/mod.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index a90d309acd7e3..60c7790f91851 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -507,7 +507,7 @@ impl WalletSubcommands { let addr = wallet.address(); sh_println!("{}", addr.to_checksum(None))?; } - Self::Derive {mnemonic, accounts, insecure} => { + Self::Derive { mnemonic, accounts, insecure } => { let format_json = shell::is_json(); let mut accounts_json = json!([]); for i in 0..accounts.unwrap_or(1) { @@ -521,25 +521,21 @@ impl WalletSubcommands { } .signer() .await?; - + match wallet { WalletSigner::Local(local_wallet) => { let address = local_wallet.address().to_checksum(None); let private_key = hex::encode(local_wallet.credential().to_bytes()); if format_json { if insecure { - accounts_json.as_array_mut().unwrap().push( - json!({ - "address": format!("{}", address), - "private_key": format!("0x{}", private_key), - }) - ); + accounts_json.as_array_mut().unwrap().push(json!({ + "address": format!("{}", address), + "private_key": format!("0x{}", private_key), + })); } else { - accounts_json.as_array_mut().unwrap().push( - json!({ - "address": format!("{}", address) - }) - ); + accounts_json.as_array_mut().unwrap().push(json!({ + "address": format!("{}", address) + })); } } else { sh_println!("- Account {i}:")?; From 876b4f97c4629038222c4586a36f25dec30b37f8 Mon Sep 17 00:00:00 2001 From: leovct Date: Mon, 1 Dec 2025 06:37:07 +0000 Subject: [PATCH 3/5] chore: nit --- crates/cast/src/cmd/wallet/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index 60c7790f91851..4a43f92359718 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -545,9 +545,7 @@ impl WalletSubcommands { } } } - _ => { - eyre::bail!("Expected local wallet for mnemonic derivation"); - } + _ => eyre::bail!("Only local wallets are supported by this command"), } } From 5e8784b7a1f2db3214afca9f6428b4110582b2b3 Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 2 Dec 2025 06:05:16 +0000 Subject: [PATCH 4/5] feat: implement tests --- crates/cast/src/cmd/wallet/mod.rs | 6 +- crates/cast/tests/cli/main.rs | 110 ++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index 4a43f92359718..7975a7f474781 100644 --- a/crates/cast/src/cmd/wallet/mod.rs +++ b/crates/cast/src/cmd/wallet/mod.rs @@ -539,9 +539,11 @@ impl WalletSubcommands { } } else { sh_println!("- Account {i}:")?; - sh_println!("Address: {}", address)?; if insecure { - sh_println!("Private key: 0x{}", private_key)?; + sh_println!("Address: {}", address)?; + sh_println!("Private key: 0x{}\n", private_key)?; + } else { + sh_println!("Address: {}\n", address)?; } } } diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index d76b6f51592f9..144931a3e8d19 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -904,6 +904,116 @@ casttest!(wallet_mnemonic_from_entropy_json_verbose, |_prj, cmd| { "#]]); }); +// tests that `cast wallet derive` outputs the addresses of the accounts derived from the mnemonic +casttest!(wallet_derive_mnemonic, |_prj, cmd| { + cmd.args([ + "wallet", + "derive", + "--accounts", + "3", + "test test test test test test test test test test test junk", + ]) + .assert_success() + .stdout_eq(str![[r#" +- Account 0: +[ADDRESS] + +- Account 1: +[ADDRESS] + +- Account 2: +[ADDRESS] + + +"#]]); +}); + +// tests that `cast wallet derive` with insecure flag outputs the addresses and private keys of the accounts derived from the mnemonic +casttest!(wallet_derive_mnemonic_insecure, |_prj, cmd| { + cmd.args([ + "wallet", + "derive", + "--accounts", + "3", + "--insecure", + "test test test test test test test test test test test junk", + ]) + .assert_success() + .stdout_eq(str![[r#" +- Account 0: +[ADDRESS] +[PRIVATE_KEY] + +- Account 1: +[ADDRESS] +[PRIVATE_KEY] + +- Account 2: +[ADDRESS] +[PRIVATE_KEY] + + +"#]]); +}); + +// tests that `cast wallet derive` with json flag outputs the addresses of the accounts derived from the mnemonic in JSON format +casttest!(wallet_derive_mnemonic_json, |_prj, cmd| { + cmd.args([ + "wallet", + "derive", + "--accounts", + "3", + "--json", + "test test test test test test test test test test test junk", + ]) + .assert_success() + .stdout_eq(str![[r#" +[ + { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + { + "address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + }, + { + "address": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" + } +] + +"#]]); +}); + +// tests that `cast wallet derive` with insecure and json flag outputs the addresses and private keys of the accounts derived from the mnemonic in JSON format +casttest!(wallet_derive_mnemonic_insecure_json, |_prj, cmd| { + cmd.args([ + "wallet", + "derive", + "--accounts", + "3", + "--insecure", + "--json", + "test test test test test test test test test test test junk", + ]) + .assert_success() + .stdout_eq(str![[r#" +[ + { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + }, + { + "address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "private_key": "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + }, + { + "address": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "private_key": "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" + } +] + +"#]]); +}); + // tests that `cast wallet private-key` with arguments outputs the private key casttest!(wallet_private_key_from_mnemonic_arg, |_prj, cmd| { cmd.args([ From 9338939bb59480dfb914dd65e981c80ffe749239 Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 2 Dec 2025 06:09:48 +0000 Subject: [PATCH 5/5] chore: cargo fmt --- crates/cast/tests/cli/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 144931a3e8d19..6c1c69812bd87 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -928,7 +928,8 @@ casttest!(wallet_derive_mnemonic, |_prj, cmd| { "#]]); }); -// tests that `cast wallet derive` with insecure flag outputs the addresses and private keys of the accounts derived from the mnemonic +// tests that `cast wallet derive` with insecure flag outputs the addresses and private keys of the +// accounts derived from the mnemonic casttest!(wallet_derive_mnemonic_insecure, |_prj, cmd| { cmd.args([ "wallet", @@ -956,7 +957,8 @@ casttest!(wallet_derive_mnemonic_insecure, |_prj, cmd| { "#]]); }); -// tests that `cast wallet derive` with json flag outputs the addresses of the accounts derived from the mnemonic in JSON format +// tests that `cast wallet derive` with json flag outputs the addresses of the accounts derived from +// the mnemonic in JSON format casttest!(wallet_derive_mnemonic_json, |_prj, cmd| { cmd.args([ "wallet", @@ -983,7 +985,8 @@ casttest!(wallet_derive_mnemonic_json, |_prj, cmd| { "#]]); }); -// tests that `cast wallet derive` with insecure and json flag outputs the addresses and private keys of the accounts derived from the mnemonic in JSON format +// tests that `cast wallet derive` with insecure and json flag outputs the addresses and private +// keys of the accounts derived from the mnemonic in JSON format casttest!(wallet_derive_mnemonic_insecure_json, |_prj, cmd| { cmd.args([ "wallet",