diff --git a/crates/cast/src/cmd/wallet/mod.rs b/crates/cast/src/cmd/wallet/mod.rs index d7f7ffefe9d5f..7975a7f474781 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,54 @@ 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}:")?; + if insecure { + sh_println!("Address: {}", address)?; + sh_println!("Private key: 0x{}\n", private_key)?; + } else { + sh_println!("Address: {}\n", address)?; + } + } + } + _ => eyre::bail!("Only local wallets are supported by this command"), + } + } + + 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 { diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 695d8425a1d8f..c17461104d537 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -904,6 +904,119 @@ 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([