diff --git a/CHANGELOG.md b/CHANGELOG.md index bfbb1804..2859b7ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Add capacity to create FeeRate from sats/kvbytes and sats/kwu. - Rename `as_sat_vb` to `as_sat_per_vb`. Move all `FeeRate` test to `types.rs`. +- Add custom Harware Wallet Signer `HwiSigner` in `src/wallet/harwaresigner/` module. ## [v0.21.0] - [v0.20.0] diff --git a/Cargo.toml b/Cargo.toml index d61e4f37..5fcea942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o cc = { version = ">=1.0.64", optional = true } socks = { version = "0.3", optional = true } lazy_static = { version = "1.4", optional = true } +hwi = { version = "0.2.2", optional = true } bip39 = { version = "1.0.1", optional = true } bitcoinconsensus = { version = "0.19.0-3", optional = true } @@ -61,6 +62,7 @@ key-value-db = ["sled"] all-keys = ["keys-bip39"] keys-bip39 = ["bip39"] rpc = ["bitcoincore-rpc"] +hardware-signer = ["hwi"] # We currently provide mulitple implementations of `Blockchain`, all are # blocking except for the `EsploraBlockchain` which can be either async or @@ -93,6 +95,7 @@ test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-bl test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"] test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"] test-md-docs = ["electrum"] +test-hardware-signer = ["hardware-signer"] [dev-dependencies] lazy_static = "1.4" diff --git a/src/wallet/hardwaresigner.rs b/src/wallet/hardwaresigner.rs new file mode 100644 index 00000000..7e4f74bc --- /dev/null +++ b/src/wallet/hardwaresigner.rs @@ -0,0 +1,64 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! HWI Signer +//! +//! This module contains a simple implementation of a Custom signer for rust-hwi + +use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::secp256k1::{All, Secp256k1}; +use bitcoin::util::bip32::Fingerprint; + +use hwi::error::Error; +use hwi::types::{HWIChain, HWIDevice}; +use hwi::HWIClient; + +use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; + +#[derive(Debug)] +/// Custom signer for Hardware Wallets +/// +/// This ignores `sign_options` and leaves the decisions up to the hardware wallet. +pub struct HWISigner { + fingerprint: Fingerprint, + client: HWIClient, +} + +impl HWISigner { + /// Create a instance from the specified device and chain + pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { + let client = HWIClient::get_client(device, false, chain)?; + Ok(HWISigner { + fingerprint: device.fingerprint, + client, + }) + } +} + +impl SignerCommon for HWISigner { + fn id(&self, _secp: &Secp256k1) -> SignerId { + SignerId::Fingerprint(self.fingerprint) + } +} + +/// This implementation ignores `sign_options` +impl TransactionSigner for HWISigner { + fn sign_transaction( + &self, + psbt: &mut PartiallySignedTransaction, + _sign_options: &crate::SignOptions, + _secp: &crate::wallet::utils::SecpCtx, + ) -> Result<(), SignerError> { + psbt.combine(self.client.sign_tx(psbt)?.psbt) + .expect("Failed to combine HW signed psbt with passed PSBT"); + Ok(()) + } +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 3506b1f5..7bd2d6ba 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -48,6 +48,9 @@ pub(crate) mod utils; #[cfg_attr(docsrs, doc(cfg(feature = "verify")))] pub mod verify; +#[cfg(feature = "hardware-signer")] +pub mod hardwaresigner; + pub use utils::IsDust; #[allow(deprecated)] @@ -5414,4 +5417,33 @@ pub(crate) mod test { // ...and checking that everything is fine assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate); } + + #[cfg(feature = "test-hardware-signer")] + #[test] + fn test_create_signer() { + use crate::wallet::hardwaresigner::HWISigner; + use hwi::types::HWIChain; + use hwi::HWIClient; + + let devices = HWIClient::enumerate().unwrap(); + let device = devices.first().expect("No devices found"); + let client = HWIClient::get_client(device, true, HWIChain::Regtest).unwrap(); + let descriptors = client.get_descriptors(None).unwrap(); + let custom_signer = HWISigner::from_device(device, HWIChain::Regtest).unwrap(); + + let (mut wallet, _, _) = get_funded_wallet(&descriptors.internal[0]); + wallet.add_signer( + KeychainKind::External, + SignerOrdering(200), + Arc::new(custom_signer), + ); + + let addr = wallet.get_address(LastUnused).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert!(finalized); + } } diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index 7548b321..1704a953 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -159,6 +159,16 @@ pub enum SignerError { InvalidSighash, /// Error while computing the hash to sign SighashError(sighash::Error), + /// Error while signing using hardware wallets + #[cfg(feature = "hardware-signer")] + HWIError(hwi::error::Error), +} + +#[cfg(feature = "hardware-signer")] +impl From for SignerError { + fn from(e: hwi::error::Error) -> Self { + SignerError::HWIError(e) + } } impl From for SignerError {