diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0661163..630ca8a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,9 +42,10 @@ jobs: matrix: rust: - version: stable # STABLE - features: miniscript - version: 1.63.0 # MSRV - features: miniscript + features: + - miniscript + - signer emulator: - name: trezor - name: ledger @@ -79,12 +80,12 @@ jobs: - name: Download and Install HWI run: ./get_hwi.sh - name: Test python - run: cargo test --features ${{ matrix.rust.features }} test_python -- --test-threads=1 + run: cargo test --features ${{ matrix.features }} test_python -- --test-threads=1 - name: Restart simulator if: matrix.emulator.name == 'ledger' run: docker restart simulator - name: Test binary - run: cargo test --features ${{ matrix.rust.features }} test_binary -- --test-threads=1 + run: cargo test --features ${{ matrix.features }} test_binary -- --test-threads=1 - name: Wipe python run: cargo test test_wipe_device_pyhton -- --ignored --test-threads=1 - name: Restart simulator diff --git a/Cargo.toml b/Cargo.toml index 4acfc6c..be82969 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hwi" -version = "0.9.0" +version = "0.10.0" authors = ["Daniela Brozzoni "] edition = "2018" license = "MIT" @@ -12,10 +12,11 @@ readme = "README.md" [dependencies] bitcoin = { version = "0.32", features = ["serde", "base64"] } +pyo3 = { version = "0.21.2", features = ["auto-initialize"] } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } -pyo3 = { version = "0.21.2", features = ["auto-initialize"] } +bdk_wallet = { version = "1.0.0-beta.3", optional = true } miniscript = { version = "12.0", features = ["serde"], optional = true } [dev-dependencies] @@ -23,3 +24,5 @@ serial_test = "0.6.0" [features] doctest = [] +signer = ["dep:bdk_wallet"] +miniscript = ["dep:miniscript"] diff --git a/src/lib.rs b/src/lib.rs index 2181c50..349182c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ -//! Rust wrapper for the [Bitcoin Hardware Wallet Interface](https://github.com/bitcoin-core/HWI/). +//! This crate is contains both: +//! - [`HWIClient`]: A Rust wrapper for the [Bitcoin Hardware Wallet Interface](https://github.com/bitcoin-core/HWI/). +//! - [`HWISigner`]: An implementation of a [`TransactionSigner`] to be used with hardware wallets, that relies on [`HWIClient`]. //! -//! # Example - display address with path +//! # HWIClient Example: +//! ## Display address with path //! ```no_run //! use bitcoin::bip32::{ChildNumber, DerivationPath}; //! use hwi::error::Error; @@ -30,6 +33,46 @@ //! Ok(()) //! } //! ``` +//! +//! # HWISigner Example: +//! ## Add custom [`HWISigner`] to [`Wallet`] +//! ```no_run +//! # #[cfg(feature = "signer")] +//! # { +//! use bdk_wallet::bitcoin::Network; +//! use bdk_wallet::descriptor::Descriptor; +//! use bdk_wallet::signer::SignerOrdering; +//! use bdk_wallet::{KeychainKind, SignOptions, Wallet}; +//! use hwi::{HWIClient, HWISigner}; +//! use std::str::FromStr; +//! use std::sync::Arc; +//! +//! fn main() -> Result<(), Box> { +//! let mut devices = HWIClient::enumerate()?; +//! if devices.is_empty() { +//! panic!("No devices found!"); +//! } +//! let first_device = devices.remove(0)?; +//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?; +//! +//! let mut wallet = Wallet::create("", "") +//! .network(Network::Testnet) +//! .create_wallet_no_persist()?; +//! +//! // Adding the hardware signer to the BDK wallet +//! wallet.add_signer( +//! KeychainKind::External, +//! SignerOrdering(200), +//! Arc::new(custom_signer), +//! ); +//! +//! Ok(()) +//! } +//! # } +//! ``` +//! +//! [`TransactionSigner`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/signer/trait.TransactionSigner.html +//! [`Wallet`]: https://docs.rs/bdk_wallet/1.0.0-beta.1/bdk_wallet/struct.Wallet.html #[cfg(test)] #[macro_use] @@ -37,12 +80,16 @@ extern crate serial_test; extern crate core; pub use interface::HWIClient; +#[cfg(feature = "signer")] +pub use signer::HWISigner; #[cfg(feature = "doctest")] pub mod doctest; pub mod error; pub mod implementations; pub mod interface; +#[cfg(feature = "signer")] +pub mod signer; pub mod types; #[cfg(test)] diff --git a/src/signer.rs b/src/signer.rs new file mode 100644 index 0000000..c2854d7 --- /dev/null +++ b/src/signer.rs @@ -0,0 +1,55 @@ +use bdk_wallet::bitcoin::bip32::Fingerprint; +use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1}; +use bdk_wallet::bitcoin::Psbt; + +use crate::error::Error; +use crate::types::{HWIChain, HWIDevice}; +use crate::HWIClient; + +use bdk_wallet::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 an 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) + } +} + +impl TransactionSigner for HWISigner { + fn sign_transaction( + &self, + psbt: &mut Psbt, + _sign_options: &bdk_wallet::SignOptions, + _secp: &Secp256k1, + ) -> Result<(), SignerError> { + psbt.combine( + self.client + .sign_tx(psbt) + .map_err(|e| { + SignerError::External(format!("While signing with hardware wallet: {}", e)) + })? + .psbt, + ) + .expect("Failed to combine HW signed psbt with passed PSBT"); + Ok(()) + } +}