Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
- Fixed `warn!` messages, including the mainnet notice, being hidden unless `RUST_LOG` was set; warnings are now shown by default
- Added `hwi` feature with a hardware wallet `hwi` command to list connected devices, register a wallet policy, display a receive address, and sign PSBTs. Supports Ledger, Coldcard, BitBox02, Jade and Specter.
- Replaced plain `wallets` with `wallets list`, and added `wallets delete <wallet_name>` for unused saved wallet configurations.
- Fixed `key generate` silently producing a 24-word mnemonic for any `--entropy` value other than 12; 15, 18 and 21 are now supported and invalid values are rejected

## [4.0.0]

Expand Down
39 changes: 31 additions & 8 deletions src/handlers/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use bdk_wallet::keys::{
DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey, bip39::WordCount,
};
use bdk_wallet::miniscript::{self, Segwitv0};
use clap::Parser;
use clap::{Parser, ValueEnum};

impl KeySubCommand {
pub fn execute(&self, ctx: &mut AppContext<Init>) -> Result<(), Error> {
Expand All @@ -26,6 +26,34 @@ impl KeySubCommand {
}
}
}
/// Mnemonic lengths defined by BIP39. A local mirror of `WordCount`, which
/// doesn't derive `Clone` and so can't be parsed by clap directly.
#[derive(Clone, Copy, ValueEnum, Debug, Eq, PartialEq)]
pub enum MnemonicWordCount {
#[value(name = "12")]
Words12,
#[value(name = "15")]
Words15,
#[value(name = "18")]
Words18,
#[value(name = "21")]
Words21,
#[value(name = "24")]
Words24,
}

impl From<MnemonicWordCount> for WordCount {
fn from(word_count: MnemonicWordCount) -> Self {
match word_count {
MnemonicWordCount::Words12 => WordCount::Words12,
MnemonicWordCount::Words15 => WordCount::Words15,
MnemonicWordCount::Words18 => WordCount::Words18,
MnemonicWordCount::Words21 => WordCount::Words21,
MnemonicWordCount::Words24 => WordCount::Words24,
}
}
}

#[derive(Parser, Debug, Clone, PartialEq, Eq)]
pub struct GenerateKeyCommand {
/// Entropy level based on number of random seed mnemonic words.
Expand All @@ -35,7 +63,7 @@ pub struct GenerateKeyCommand {
long = "entropy",
default_value = "12"
)]
word_count: usize,
word_count: MnemonicWordCount,
/// Seed password.
#[arg(env = "PASSWORD", short = 'p', long = "password")]
password: Option<String>,
Expand All @@ -46,13 +74,8 @@ impl AppCommand<AppContext<Init>> for GenerateKeyCommand {

fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> {
let secp = Secp256k1::new();
let mnemonic_type = match self.word_count {
12 => WordCount::Words12,
_ => WordCount::Words24,
};

let mnemonic: GeneratedKey<_, miniscript::BareCtx> =
Mnemonic::generate((mnemonic_type, Language::English))
Mnemonic::generate((self.word_count.into(), Language::English))
.map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?;
let mnemonic = mnemonic.into_key();
let xkey: ExtendedKey = (mnemonic.clone(), self.password.clone()).into_extended_key()?;
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,37 @@ mod test_key {
.stdout(predicate::str::contains("\"fingerprint\":"));
}

#[test]
fn test_cli_key_generate_word_counts() {
let temp_dir = TempDir::new().unwrap();
let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf()));

for word_count in [12, 15, 18, 21, 24] {
let output = cli
.key_cmd(&["generate", "--entropy", &word_count.to_string()])
.output()
.expect("Failed to execute generate command");
assert!(output.status.success(), "Generate command failed");

let json: Value = serde_json::from_slice(&output.stdout).expect("Invalid JSON");
let mnemonic = json["mnemonic"].as_str().expect("Missing mnemonic");
assert_eq!(mnemonic.split_whitespace().count(), word_count);
}
}

#[test]
fn test_cli_key_generate_rejects_invalid_word_count() {
let temp_dir = TempDir::new().unwrap();
let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf()));

for word_count in ["0", "13", "999"] {
cli.key_cmd(&["generate", "--entropy", word_count])
.assert()
.failure()
.stderr(predicate::str::contains("possible values"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an unnecessary assertion but can be overlooked

}
}

#[test]
fn test_cli_key_derive() {
let temp_dir = TempDir::new().unwrap();
Expand Down