From 100ea8d7e8ae789e3c328706591ec20ba427496b Mon Sep 17 00:00:00 2001 From: Richard Ulrich Date: Fri, 6 Jan 2023 12:37:07 +0100 Subject: [PATCH] adding hardware signers --- Cargo.lock | 122 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 +- src/commands.rs | 3 ++ src/handlers.rs | 27 +++++++++++ src/utils.rs | 46 ++++++++++++++++++ 5 files changed, 200 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 761b4acc..f4af6d25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,7 @@ dependencies = [ "esplora-client", "futures", "getrandom", + "hwi", "js-sys", "log", "miniscript", @@ -954,6 +955,21 @@ dependencies = [ "quick-error", ] +[[package]] +name = "hwi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefe6626545c823e8cae0d3ad06bdface90571a7fde82ca1c1d17347388a5c0" +dependencies = [ + "base64 0.13.1", + "bitcoin", + "miniscript", + "once_cell", + "pyo3", + "serde", + "serde_json", +] + [[package]] name = "hyper" version = "0.14.24" @@ -1012,6 +1028,29 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indoc" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" +dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", + "unindent", +] + [[package]] name = "instant" version = "0.1.12" @@ -1252,9 +1291,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] name = "opaque-debug" @@ -1349,6 +1388,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + [[package]] name = "pbkdf2" version = "0.11.0" @@ -1415,6 +1473,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.51" @@ -1424,6 +1488,54 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pyo3" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41d50a7271e08c7c8a54cd24af5d62f73ee3a6f6a314215281ebdec421d5752" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "parking_lot", + "paste", + "pyo3-build-config", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779239fc40b8e18bc8416d3a37d280ca9b9fb04bda54b98037bb6748595c2410" +dependencies = [ + "once_cell", +] + +[[package]] +name = "pyo3-macros" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b247e8c664be87998d8628e86f282c25066165f1f8dda66100c48202fdb93a" +dependencies = [ + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8c2812c412e00e641d99eeb79dd478317d981d938aa60325dfa7157b607095" +dependencies = [ + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2213,6 +2325,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unindent" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 6e9693ea..95e1494c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,9 @@ esplora-reqwest = ["esplora", "bdk/use-esplora-reqwest", "bdk/reqwest-default-tl # Use this to consensus verify transactions at sync time verify = ["bdk/verify"] +# Use hardware wallets to sign transactions +hardware-signer = ["bdk/hardware-signer"] + # Extra utility tools # Compile policies compiler = ["bdk/compiler"] @@ -93,4 +96,4 @@ regtest-electrum = ["regtest-node", "electrum", "electrsd/electrs_0_8_10"] # This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended # for libraries to explicitly include the "getrandom/js" feature, so we only do it when # necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support -dev-getrandom-wasm = ["getrandom/js"] \ No newline at end of file +dev-getrandom-wasm = ["getrandom/js"] diff --git a/src/commands.rs b/src/commands.rs index 6b64c87f..c8d7c0b1 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -565,6 +565,9 @@ pub enum KeySubCommand { #[clap(name = "PATH", short = 'p', long = "path")] path: DerivationPath, }, + #[cfg(feature = "hardware-signer")] + /// List the public descriptors of the available hardware wallets + Hardware {}, } /// Subcommands available in REPL mode. diff --git a/src/handlers.rs b/src/handlers.rs index b7897c0e..fb69df25 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -50,6 +50,11 @@ use bdk::descriptor::Segwitv0; use bdk::descriptor::{Descriptor, Legacy, Miniscript}; #[cfg(all(feature = "reserves", feature = "electrum"))] use bdk::electrum_client::{Client, ElectrumApi}; +#[cfg(feature = "hardware-signer")] +use bdk::hwi::{ + interface::HWIClient, + types::{HWIChain, HWIDescriptor}, +}; use bdk::keys::bip39::{Language, Mnemonic, WordCount}; use bdk::keys::DescriptorKey::Secret; use bdk::keys::KeyError::{InvalidNetwork, Message}; @@ -57,6 +62,8 @@ use bdk::keys::{DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, Genera use bdk::miniscript::miniscript; #[cfg(feature = "compiler")] use bdk::miniscript::policy::Concrete; +#[cfg(feature = "hardware-signer")] +use bdk::wallet::signer::SignerError; use bdk::SignOptions; #[cfg(all(feature = "reserves", feature = "electrum"))] use bdk::{bitcoin::Address, blockchain::Capability}; @@ -487,6 +494,26 @@ pub(crate) fn handle_key_subcommand( Err(Error::Key(Message("Invalid key variant".to_string()))) } } + #[cfg(feature = "hardware-signer")] + KeySubCommand::Hardware {} => { + let chain = match network { + Network::Bitcoin => HWIChain::Main, + Network::Testnet => HWIChain::Test, + Network::Regtest => HWIChain::Regtest, + Network::Signet => HWIChain::Signet, + }; + let devices = HWIClient::enumerate().map_err(|e| SignerError::from(e))?; + let descriptors = devices.iter().map(|device_| { + let device = device_.clone() + .map_err(|e| SignerError::from(e))?; + let client = HWIClient::get_client(&device, true, chain.clone()) + .map_err(|e| SignerError::from(e))?; + let descriptors: HWIDescriptor = client.get_descriptors(None) + .map_err(|e| SignerError::from(e))?; + Ok(json!({"device": device.model, "receiving": descriptors.receive[0].to_string(), "change": descriptors.internal[0]})) + }).collect::, Error>>()?; + Ok(json!(descriptors)) + } } } diff --git a/src/utils.rs b/src/utils.rs index fa4e6c1e..e29fed5a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -43,8 +43,18 @@ use bdk::database::any::SledDbConfiguration; #[cfg(feature = "sqlite-db")] use bdk::database::any::SqliteDbConfiguration; use bdk::database::{AnyDatabase, AnyDatabaseConfig, BatchDatabase, ConfigurableDatabase}; +#[cfg(feature = "hardware-signer")] +use bdk::hwi::{interface::HWIClient, types::HWIChain}; +#[cfg(feature = "hardware-signer")] +use bdk::wallet::hardwaresigner::HWISigner; +#[cfg(feature = "hardware-signer")] +use bdk::wallet::signer::{SignerError, SignerOrdering}; use bdk::wallet::wallet_name_from_descriptor; +#[cfg(feature = "hardware-signer")] +use bdk::KeychainKind; use bdk::{Error, Wallet}; +#[cfg(feature = "hardware-signer")] +use std::sync::Arc; /// Create a randomized wallet name from the descriptor checksum. /// If wallet options already includes a name, use that instead. @@ -497,5 +507,41 @@ where let descriptor = wallet_opts.descriptor.as_str(); let change_descriptor = wallet_opts.change_descriptor.as_deref(); let wallet = Wallet::new(descriptor, change_descriptor, network, database)?; + + #[cfg(feature = "hardware-signer")] + let wallet = add_hardware_signers(wallet, network)?; + + Ok(wallet) +} + +/// Add hardware wallets as signers to the wallet +#[cfg(feature = "hardware-signer")] +fn add_hardware_signers(wallet: Wallet, network: Network) -> Result, Error> +where + D: BatchDatabase, +{ + let mut wallet = wallet; + let chain = match network { + Network::Bitcoin => HWIChain::Main, + Network::Testnet => HWIChain::Test, + Network::Regtest => HWIChain::Regtest, + Network::Signet => HWIChain::Signet, + }; + let devices = HWIClient::enumerate().map_err(|e| SignerError::from(e))?; + for device in devices { + let device = device.map_err(|e| SignerError::from(e))?; + // Creating a custom signer from the device + let custom_signer = + HWISigner::from_device(&device, chain.clone()).map_err(|e| SignerError::from(e))?; + + // Adding the hardware signer to the BDK wallet + wallet.add_signer( + KeychainKind::External, + SignerOrdering(200), + Arc::new(custom_signer), + ); + println!("Added {} as a signer to the wallet.", device.model); + } + Ok(wallet) }