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",
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<String, Error> {
- 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<Self::Output, Error> {
+ 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<Self::Output, Error> {
+ let config = match WalletConfig::load(&ctx.datadir)? {
+ Some(cfg) => cfg,
+ None => return Err(Error::Generic("No wallets configured yet.".into())),
+ };
+
+ Ok(WalletsListResult(config.wallets))
+ }
}
+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<String>,
- pretty: bool,
-) -> Result<String, Error> {
- 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<Self::Output, Error> {
+ 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<String, Error> {
- let policy = Concrete::<String>::from_str(policy.as_str())?;
- let legacy_policy: Miniscript<String, Legacy> = policy
- .compile()
- .map_err(|e| Error::Generic(e.to_string()))?;
- let segwit_policy: Miniscript<String, Segwitv0> = policy
- .compile()
- .map_err(|e| Error::Generic(e.to_string()))?;
- let taproot_policy: Miniscript<String, Tap> = 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<Self::Output, Error> {
+ let policy: Concrete<String> = 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<String, bdk_wallet::miniscript::Legacy> = policy
+ .compile()
+ .map_err(|e| Error::Generic(e.to_string()))?;
+ let segwit_policy: Miniscript<String, bdk_wallet::miniscript::Segwitv0> = policy
+ .compile()
+ .map_err(|e| Error::Generic(e.to_string()))?;
+ let taproot_policy: Miniscript<String, bdk_wallet::miniscript::Tap> = 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,
+ })
+ }
}
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<String, Error> {
- 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<Segwitv0> =
- 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<String>,
+}
+
+impl AppCommand for GenerateKeyCommand {
+ type Output = KeyResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> 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))
+ .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<Self::Output, Error> {
+ 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<Segwitv0> =
+ 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<String>,
+}
+
+impl AppCommand for RestoreKeyCommand {
+ type Output = KeyResult;
+
+ fn execute(&self, ctx: &mut AppContext) -> Result<Self::Output, Error> {
+ 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,
+ })
+ }
+}