]> Untitled Git - bdk-cli/commitdiff
ref(handlers): refactor config, descr and key mods
authorVihiga Tyonum <withtvpeter@gmail.com>
Thu, 21 May 2026 15:22:04 +0000 (16:22 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Fri, 5 Jun 2026 16:32:14 +0000 (17:32 +0100)
- move execution logic into config, desc and key
modules

src/handlers/config.rs
src/handlers/descriptor.rs
src/handlers/key.rs

index e64d1654539255b63f418221a8dba6ce7d4deff0..fe64132f6ad97cb72f9d81f1c072c5df8dae2ba6 100644 (file)
@@ -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<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))
+    }
 }
index 9babbc8e3ebeacced98587cef7c6e7efeaea27a0..188abd8168aaad7bd5d69a93771fbdbc40235151 100644 (file)
+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,
+        })
+    }
 }
index abf3833b4922918ec3769b610c612dc7a0a513b7..6f3960ee955bf62b3126b2b1967504be11908024 100644 (file)
 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,
+        })
+    }
+}