From: Vihiga Tyonum Date: Thu, 21 May 2026 15:22:04 +0000 (+0100) Subject: ref(handlers): refactor config, descr and key mods X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/FromScriptException.UnrecognizedScript.html?a=commitdiff_plain;h=dcaeb5534b45e0fd9f46126d9f45556fd0121a92;p=bdk-cli ref(handlers): refactor config, descr and key mods - move execution logic into config, desc and key modules --- diff --git a/src/handlers/config.rs b/src/handlers/config.rs index e64d165..fe64132 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -1,14 +1,4 @@ use std::collections::HashMap; -use std::path::Path; - -#[cfg(any(feature = "sqlite", feature = "redb"))] -#[cfg(feature = "sqlite")] -use crate::commands::DatabaseType; -use crate::commands::WalletOpts; -use crate::config::{WalletConfig, WalletConfigInner}; -use crate::error::BDKCliError as Error; -use bdk_wallet::bitcoin::Network; -use serde_json::json; #[cfg(any( feature = "electrum", @@ -16,121 +6,153 @@ use serde_json::json; feature = "rpc", feature = "cbf" ))] -use crate::commands::ClientType; - -/// Handle wallet config subcommand to create or update config.toml -pub fn handle_config_subcommand( - datadir: &Path, - network: Network, - wallet: String, - wallet_opts: &WalletOpts, - force: bool, -) -> Result { - if network == Network::Bitcoin { - eprintln!( - "WARNING: You are configuring a wallet for Bitcoin MAINNET. - This software is experimental and not recommended for use with real funds. - Consider using a testnet for testing purposes. \n" - ); - } +use crate::client::ClientType; +use crate::commands::WalletOpts; +use crate::config::{WalletConfig, WalletConfigInner}; +use crate::error::BDKCliError as Error; +use crate::handlers::{AppCommand, AppContext}; +#[cfg(feature = "sqlite")] +use crate::persister::DatabaseType; +use crate::utils::types::{StatusResult, WalletsListResult}; +use bdk_wallet::bitcoin::Network; +use clap::Args; + +#[derive(Args, Debug, Clone, PartialEq)] +pub struct SaveConfigCommand { + /// Overwrite existing wallet configuration if it exists. + #[arg(short = 'f', long = "force", default_value_t = false)] + pub(crate) force: bool, + + #[command(flatten)] + pub(crate) wallet_opts: WalletOpts, +} + +impl AppCommand for SaveConfigCommand { + type Output = StatusResult; + + fn execute(&self, ctx: &mut AppContext<'_>) -> Result { + if ctx.network == Network::Bitcoin { + eprintln!("WARNING: Configuring for Bitcoin MAINNET. Experimental software!"); + } + + let wallet_name = match &self.wallet_opts.wallet { + Some(wallet) => wallet, + None => return Err(Error::Generic("wallet is required".to_owned())), + }; - let ext_descriptor = wallet_opts.ext_descriptor.clone(); - let int_descriptor = wallet_opts.int_descriptor.clone(); + let ext_descriptor = self.wallet_opts.ext_descriptor.clone(); + let int_descriptor = self.wallet_opts.int_descriptor.clone(); - if ext_descriptor.contains("xprv") || ext_descriptor.contains("tprv") { - eprintln!( - "WARNING: Your external descriptor contains PRIVATE KEYS. + if ext_descriptor.contains("xprv") || ext_descriptor.contains("tprv") { + eprintln!( + "WARNING: Your external descriptor contains PRIVATE KEYS. Private keys will be saved in PLAINTEXT in the config file. This is a security risk. Consider using public descriptors instead.\n" - ); - } + ); + } - if let Some(ref internal_desc) = int_descriptor - && (internal_desc.contains("xprv") || internal_desc.contains("tprv")) - { - eprintln!( - "WARNING: Your internal descriptor contains PRIVATE KEYS. + if let Some(ref internal_desc) = int_descriptor + && (internal_desc.contains("xprv") || internal_desc.contains("tprv")) + { + eprintln!( + "WARNING: Your internal descriptor contains PRIVATE KEYS. Private keys will be saved in PLAINTEXT in the config file. This is a security risk. Consider using public descriptors instead.\n" - ); - } + ); + } - let mut config = WalletConfig::load(datadir)?.unwrap_or(WalletConfig { - wallets: HashMap::new(), - }); + let mut config = WalletConfig::load(&ctx.datadir)?.unwrap_or(WalletConfig { + wallets: HashMap::new(), + }); - if config.wallets.contains_key(&wallet) && !force { - return Err(Error::Generic(format!( - "Wallet '{wallet}' already exists in config.toml. Use --force to overwrite." - ))); - } + if config.wallets.contains_key(wallet_name.as_str()) && !self.force { + return Err(Error::Generic(format!( + "Wallet '{}' already exists. Use --force to overwrite.", + wallet_name + ))); + }; - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - let client_type = wallet_opts.client_type.clone(); - #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] - let url = &wallet_opts.url.clone(); - #[cfg(any(feature = "sqlite", feature = "redb"))] - let database_type = match wallet_opts.database_type { - #[cfg(feature = "sqlite")] - DatabaseType::Sqlite => "sqlite".to_string(), - #[cfg(feature = "redb")] - DatabaseType::Redb => "redb".to_string(), - }; - - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "rpc", - feature = "cbf" - ))] - let client_type = match client_type { - #[cfg(feature = "electrum")] - ClientType::Electrum => "electrum".to_string(), - #[cfg(feature = "esplora")] - ClientType::Esplora => "esplora".to_string(), - #[cfg(feature = "rpc")] - ClientType::Rpc => "rpc".to_string(), - #[cfg(feature = "cbf")] - ClientType::Cbf => "cbf".to_string(), - }; - - let wallet_config = WalletConfigInner { - wallet: wallet.clone(), - network: network.to_string(), - ext_descriptor, - int_descriptor, - #[cfg(any(feature = "sqlite", feature = "redb"))] - database_type, #[cfg(any( feature = "electrum", feature = "esplora", feature = "rpc", feature = "cbf" ))] - client_type: Some(client_type), - #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc",))] - server_url: Some(url.to_string()), - #[cfg(feature = "rpc")] - rpc_user: Some(wallet_opts.basic_auth.0.clone()), - #[cfg(feature = "rpc")] - rpc_password: Some(wallet_opts.basic_auth.1.clone()), - #[cfg(feature = "electrum")] - batch_size: Some(wallet_opts.batch_size), - #[cfg(feature = "esplora")] - parallel_requests: Some(wallet_opts.parallel_requests), - #[cfg(feature = "rpc")] - cookie: wallet_opts.cookie.clone(), - }; - - config.wallets.insert(wallet.clone(), wallet_config); - config.save(datadir)?; - - Ok(serde_json::to_string_pretty(&json!({ - "message": format!("Wallet '{wallet}' initialized successfully in {:?}", datadir.join("config.toml")) - }))?) + let client_type = match self.wallet_opts.client_type.clone() { + #[cfg(feature = "electrum")] + ClientType::Electrum => "electrum".to_string(), + #[cfg(feature = "esplora")] + ClientType::Esplora => "esplora".to_string(), + #[cfg(feature = "rpc")] + ClientType::Rpc => "rpc".to_string(), + #[cfg(feature = "cbf")] + ClientType::Cbf => "cbf".to_string(), + }; + + let wallet_config = WalletConfigInner { + wallet: wallet_name.clone(), + network: ctx.network.to_string(), + ext_descriptor: self.wallet_opts.ext_descriptor.clone(), + int_descriptor: self.wallet_opts.int_descriptor.clone(), + + #[cfg(any(feature = "sqlite", feature = "redb"))] + database_type: match self.wallet_opts.database_type { + #[cfg(feature = "sqlite")] + DatabaseType::Sqlite => "sqlite".to_string(), + #[cfg(feature = "redb")] + DatabaseType::Redb => "redb".to_string(), + }, + + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + client_type: Some(client_type), + + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + server_url: Some(self.wallet_opts.url.clone()), + + #[cfg(feature = "rpc")] + rpc_user: Some(self.wallet_opts.basic_auth.0.clone()), + #[cfg(feature = "rpc")] + rpc_password: Some(self.wallet_opts.basic_auth.1.clone()), + #[cfg(feature = "electrum")] + batch_size: Some(self.wallet_opts.batch_size), + #[cfg(feature = "esplora")] + parallel_requests: Some(self.wallet_opts.parallel_requests), + #[cfg(feature = "rpc")] + cookie: self.wallet_opts.cookie.clone(), + }; + + config.wallets.insert(wallet_name.clone(), wallet_config); + config + .save(&ctx.datadir) + .map_err(|error| Error::Generic(error.to_string()))?; + + Ok(StatusResult { + message: format!( + "Wallet '{}' initialized successfully in {:?}", + wallet_name, + ctx.datadir.join("config.toml") + ), + }) + } +} + +#[derive(Args, Debug, Clone, PartialEq)] +pub struct ListWalletsCommand {} + +impl AppCommand for ListWalletsCommand { + type Output = WalletsListResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let config = match WalletConfig::load(&ctx.datadir)? { + Some(cfg) => cfg, + None => return Err(Error::Generic("No wallets configured yet.".into())), + }; + + Ok(WalletsListResult(config.wallets)) + } } diff --git a/src/handlers/descriptor.rs b/src/handlers/descriptor.rs index 9babbc8..188abd8 100644 --- a/src/handlers/descriptor.rs +++ b/src/handlers/descriptor.rs @@ -1,105 +1,114 @@ +use crate::utils::types::DescriptorResult; use crate::{ error::BDKCliError as Error, + handlers::{AppCommand, AppContext}, utils::{ descriptors::{ generate_descriptor_from_mnemonic, generate_descriptor_with_mnemonic, generate_descriptors, }, is_mnemonic, - output::FormatOutput, }, }; - +use clap::Parser; #[cfg(feature = "compiler")] use { - crate::handlers::types::DescriptorResult, bdk_wallet::{ bitcoin::XOnlyPublicKey, - miniscript::{ - Descriptor, Legacy, Miniscript, Segwitv0, Tap, descriptor::TapTree, policy::Concrete, - }, + miniscript::{Descriptor, Miniscript, descriptor::TapTree, policy::Concrete}, }, std::{str::FromStr, sync::Arc}, }; -use bdk_wallet::bitcoin::Network; - #[cfg(feature = "compiler")] const NUMS_UNSPENDABLE_KEY_HEX: &str = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; -/// Handle the top-level `descriptor` command -pub fn handle_descriptor_command( - network: Network, - desc_type: String, +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct DescriptorCommand { + /// Descriptor type (script type) + #[arg( + long = "type", + short = 't', + value_parser = ["pkh", "wpkh", "sh", "wsh", "tr"], + default_value = "wsh" + )] + pub(crate) desc_type: String, + + /// Optional key: xprv, xpub, or mnemonic phrase key: Option, - pretty: bool, -) -> Result { - let result = match key { - Some(key) => { - if is_mnemonic(&key) { - // User provided mnemonic - generate_descriptor_from_mnemonic(&key, network, &desc_type) - } else { - // User provided xprv/xpub - generate_descriptors(&desc_type, &key, network) +} +impl AppCommand for DescriptorCommand { + type Output = DescriptorResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + match &self.key { + Some(key) => { + if is_mnemonic(key) { + generate_descriptor_from_mnemonic(key, ctx.network, &self.desc_type) + } else { + generate_descriptors(&self.desc_type, key, ctx.network) + } } + None => generate_descriptor_with_mnemonic(ctx.network, &self.desc_type), } - // Generate new mnemonic and descriptors - None => generate_descriptor_with_mnemonic(network, &desc_type), - }?; - result.format(pretty) + } } -/// Handle the miniscript compiler sub-command -/// -/// Compiler options are described in [`CliSubCommand::Compile`]. #[cfg(feature = "compiler")] -pub(crate) fn handle_compile_subcommand( - _network: Network, +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct CompileCommand { + /// Sets the spending policy to compile. + #[arg(env = "POLICY", required = true, index = 1)] policy: String, + /// Sets the script type used to embed the compiled policy. + #[arg(env = "TYPE", short = 't', long = "type", default_value = "wsh", value_parser = ["sh","wsh", "sh-wsh", "tr"] + )] script_type: String, - pretty: bool, -) -> Result { - let policy = Concrete::::from_str(policy.as_str())?; - let legacy_policy: Miniscript = policy - .compile() - .map_err(|e| Error::Generic(e.to_string()))?; - let segwit_policy: Miniscript = policy - .compile() - .map_err(|e| Error::Generic(e.to_string()))?; - let taproot_policy: Miniscript = policy - .compile() - .map_err(|e| Error::Generic(e.to_string()))?; +} - let descriptor = match script_type.as_str() { - "sh" => Descriptor::new_sh(legacy_policy), - "wsh" => Descriptor::new_wsh(segwit_policy), - "sh-wsh" => Descriptor::new_sh_wsh(segwit_policy), - "tr" => { - // For tr descriptors, we use a well-known unspendable key (NUMS point). - // This ensures the key path is effectively disabled and only script path can be used. - // See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs +#[cfg(feature = "compiler")] +impl AppCommand for CompileCommand { + type Output = DescriptorResult; - let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX) - .map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?; + fn execute(&self, _ctx: &mut AppContext) -> Result { + let policy: Concrete = Concrete::from_str(&self.policy) + .map_err(|e| Error::Generic(format!("Invalid policy: {e}")))?; - let tree = TapTree::Leaf(Arc::new(taproot_policy)); - Descriptor::new_tr(xonly_public_key.to_string(), Some(tree)) - } - _ => { - return Err(Error::Generic( - "Invalid script type. Supported types: sh, wsh, sh-wsh, tr".to_string(), - )); - } - }?; - let result = DescriptorResult { - descriptor: Some(descriptor.to_string()), - multipath_descriptor: None, - public_descriptors: None, - private_descriptors: None, - mnemonic: None, - fingerprint: None, - }; - result.format(pretty) + let legacy_policy: Miniscript = policy + .compile() + .map_err(|e| Error::Generic(e.to_string()))?; + let segwit_policy: Miniscript = policy + .compile() + .map_err(|e| Error::Generic(e.to_string()))?; + let taproot_policy: Miniscript = policy + .compile() + .map_err(|e| Error::Generic(e.to_string()))?; + + let descriptor = match self.script_type.as_str() { + "sh" => Descriptor::new_sh(legacy_policy), + "wsh" => Descriptor::new_wsh(segwit_policy), + "sh-wsh" => Descriptor::new_sh_wsh(segwit_policy), + "tr" => { + let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX) + .map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?; + let tree = TapTree::Leaf(Arc::new(taproot_policy)); + Descriptor::new_tr(xonly_public_key.to_string(), Some(tree)) + } + _ => { + return Err(Error::Generic( + "Invalid script type. Supported: sh, wsh, sh-wsh, tr".into(), + )); + } + }?; + + Ok(DescriptorResult { + descriptor: Some(descriptor.to_string()), + mnemonic: None, + multipath_descriptor: None, + public_descriptors: None, + private_descriptors: None, + fingerprint: None, + }) + } } diff --git a/src/handlers/key.rs b/src/handlers/key.rs index abf3833..6f3960e 100644 --- a/src/handlers/key.rs +++ b/src/handlers/key.rs @@ -1,101 +1,157 @@ use crate::commands::KeySubCommand; use crate::error::BDKCliError as Error; -use crate::handlers::types::KeyResult; +use crate::handlers::{AppCommand, AppContext}; use crate::utils::output::FormatOutput; +use crate::utils::types::KeyResult; use bdk_wallet::bip39::{Language, Mnemonic}; +use bdk_wallet::bitcoin::bip32::DerivationPath; use bdk_wallet::bitcoin::bip32::KeySource; +use bdk_wallet::bitcoin::bip32::Xpriv; use bdk_wallet::bitcoin::key::Secp256k1; -use bdk_wallet::bitcoin::{Network, bip32::DerivationPath}; use bdk_wallet::keys::bip39::WordCount; use bdk_wallet::keys::{DerivableKey, GeneratableKey}; -use bdk_wallet::keys::{DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratedKey}; +use bdk_wallet::keys::{DescriptorKey, ExtendedKey, GeneratedKey}; use bdk_wallet::miniscript::{self, Segwitv0}; +use clap::Parser; -/// Handle a key sub-command -/// -/// Key sub-commands are described in [`KeySubCommand`]. -pub(crate) fn handle_key_subcommand( - network: Network, - subcommand: KeySubCommand, - pretty: bool, -) -> Result { - let secp = Secp256k1::new(); - - match subcommand { - KeySubCommand::Generate { - word_count, - password, - } => { - let mnemonic_type = match word_count { - 12 => WordCount::Words12, - _ => WordCount::Words24, - }; - let mnemonic: GeneratedKey<_, miniscript::BareCtx> = - Mnemonic::generate((mnemonic_type, Language::English)) - .map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?; - let mnemonic = mnemonic.into_key(); - let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?; - let xprv = xkey.into_xprv(network).ok_or_else(|| { - Error::Generic("Privatekey info not found (should not happen)".to_string()) - })?; - let fingerprint = xprv.fingerprint(&secp); - let phrase = mnemonic - .words() - .fold("".to_string(), |phrase, w| phrase + w + " ") - .trim() - .to_string(); - - let result = KeyResult { - xprv: xprv.to_string(), - mnemonic: Some(phrase), - fingerprint: Some(fingerprint.to_string()), - xpub: None, - }; - - result.format(pretty) - } - KeySubCommand::Restore { mnemonic, password } => { - let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)?; - let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?; - let xprv = xkey.into_xprv(network).ok_or_else(|| { - Error::Generic("Privatekey info not found (should not happen)".to_string()) - })?; - let fingerprint = xprv.fingerprint(&secp); - - let result = KeyResult { - xprv: xprv.to_string(), - mnemonic: Some(mnemonic.to_string()), - fingerprint: Some(fingerprint.to_string()), - xpub: None, - }; - result.format(pretty) - } - KeySubCommand::Derive { xprv, path } => { - if xprv.network != network.into() { - return Err(Error::Generic("Invalid network".to_string())); + + +impl KeySubCommand { + pub fn execute(&self, ctx: &mut AppContext) -> Result<(), Error> { + match self { + KeySubCommand::Generate(generate_key_command) => { + generate_key_command.execute(ctx)?.print() } - let derived_xprv = &xprv.derive_priv(&secp, &path)?; - - let origin: KeySource = (xprv.fingerprint(&secp), path); - - let derived_xprv_desc_key: DescriptorKey = - derived_xprv.into_descriptor_key(Some(origin), DerivationPath::default())?; - - if let Secret(desc_seckey, _, _) = derived_xprv_desc_key { - let desc_pubkey = desc_seckey.to_public(&secp)?; - - let result = KeyResult { - xprv: desc_seckey.to_string(), - xpub: Some(desc_pubkey.to_string()), - mnemonic: None, - fingerprint: None, - }; - result.format(pretty) - } else { - Err(Error::Generic( - "Derived key is not a secret key".to_string(), - )) + KeySubCommand::Restore(restore_key_command) => { + restore_key_command.execute(ctx)?.print() } + KeySubCommand::Derive(derive_key_command) => derive_key_command.execute(ctx)?.print(), } } } +#[derive(Parser, Debug, Clone, PartialEq, Eq)] +pub struct GenerateKeyCommand { + /// Entropy level based on number of random seed mnemonic words. + #[arg( + env = "WORD_COUNT", + short = 'e', + long = "entropy", + default_value = "12" + )] + word_count: usize, + /// Seed password. + #[arg(env = "PASSWORD", short = 'p', long = "password")] + password: Option, +} + +impl AppCommand for GenerateKeyCommand { + type Output = KeyResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + 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)) + .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()?; + let xprv = xkey.into_xprv(ctx.network).ok_or_else(|| { + Error::Generic("Privatekey info not found (should not happen)".to_string()) + })?; + let fingerprint = xprv.fingerprint(&secp); + let phrase = mnemonic + .words() + .fold("".to_string(), |phrase, w| phrase + w + " ") + .trim() + .to_string(); + + Ok(KeyResult { + xprv: xprv.to_string(), + mnemonic: Some(phrase), + fingerprint: Some(fingerprint.to_string()), + xpub: None, + }) + } +} + +#[derive(Parser, Debug, Clone, PartialEq, Eq)] +pub struct DeriveKeyCommand { + /// Extended private key to derive from. + #[arg(env = "XPRV", short = 'x', long = "xprv")] + xprv: Xpriv, + /// Path to use to derive extended public key from extended private key. + #[arg(env = "DERIVATION_PATH", short = 'p', long = "derivation_path")] + path: DerivationPath, +} + +impl AppCommand for DeriveKeyCommand { + type Output = KeyResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let secp = Secp256k1::new(); + + let derived_xprv = &self.xprv.derive_priv(&secp, &self.path)?; + + let origin: KeySource = (self.xprv.fingerprint(&secp), self.path.clone()); + + if self.xprv.network != ctx.network.into() { + return Err(Error::Generic( + "Extended key network does not match current network".to_string(), + )); + } + + let derived_xprv_desc_key: DescriptorKey = + derived_xprv.into_descriptor_key(Some(origin), DerivationPath::default())?; + + if let DescriptorKey::Secret(desc_seckey, _, _) = derived_xprv_desc_key { + let desc_pubkey = desc_seckey.to_public(&secp)?; + + Ok(KeyResult { + xprv: desc_seckey.to_string(), + xpub: Some(desc_pubkey.to_string()), + mnemonic: None, + fingerprint: None, + }) + } else { + Err(Error::Generic( + "Derived key is not a secret key".to_string(), + )) + } + } +} + +#[derive(Parser, Debug, Clone, PartialEq, Eq)] +pub struct RestoreKeyCommand { + /// Seed mnemonic words, must be quoted (eg. "word1 word2 ..."). + #[arg(env = "MNEMONIC", short = 'm', long = "mnemonic")] + mnemonic: String, + /// Seed password. + #[arg(env = "PASSWORD", short = 'p', long = "password")] + password: Option, +} + +impl AppCommand for RestoreKeyCommand { + type Output = KeyResult; + + fn execute(&self, ctx: &mut AppContext) -> Result { + let secp = Secp256k1::new(); + + let mnemonic = Mnemonic::parse_in(Language::English, &self.mnemonic)?; + let xkey: ExtendedKey = (mnemonic.clone(), &self.password).0.into_extended_key()?; + let xprv = xkey.into_xprv(ctx.network).ok_or_else(|| { + Error::Generic("Privatekey info not found (should not happen)".to_string()) + })?; + let fingerprint = xprv.fingerprint(&secp); + + Ok(KeyResult { + xprv: xprv.to_string(), + mnemonic: Some(mnemonic.to_string()), + fingerprint: Some(fingerprint.to_string()), + xpub: None, + }) + } +}