Skip to content

Commit 34eb920

Browse files
committed
Add a custom signer for hardware wallets
Also add function to get funded wallet with coinbase
1 parent c9b1b6d commit 34eb920

File tree

5 files changed

+126
-3
lines changed

5 files changed

+126
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
- Add custom Harware Wallet Signer `HwiSigner` in `src/wallet/harwaresigner/` module.
89

910
## [v0.21.0] - [v0.20.0]
1011

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ rocksdb = { version = "0.14", default-features = false, features = ["snappy"], o
3333
cc = { version = ">=1.0.64", optional = true }
3434
socks = { version = "0.3", optional = true }
3535
lazy_static = { version = "1.4", optional = true }
36+
hwi = { version = "0.2.2", optional = true }
3637

3738
bip39 = { version = "1.0.1", optional = true }
3839
bitcoinconsensus = { version = "0.19.0-3", optional = true }
@@ -61,6 +62,7 @@ key-value-db = ["sled"]
6162
all-keys = ["keys-bip39"]
6263
keys-bip39 = ["bip39"]
6364
rpc = ["bitcoincore-rpc"]
65+
hardware-signer = ["hwi"]
6466

6567
# We currently provide mulitple implementations of `Blockchain`, all are
6668
# 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
9395
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"]
9496
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
9597
test-md-docs = ["electrum"]
98+
test-hardware-signer = ["hardware-signer"]
9699

97100
[dev-dependencies]
98101
lazy_static = "1.4"

src/wallet/hardwaresigner.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Bitcoin Dev Kit
2+
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
3+
//
4+
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
5+
//
6+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
7+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
8+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
9+
// You may not use this file except in accordance with one or both of these
10+
// licenses.
11+
12+
//! HWI Signer
13+
//!
14+
//! This module contains a simple implementation of a Custom Signer for rust-hwi
15+
16+
use bitcoin::psbt::PartiallySignedTransaction;
17+
use bitcoin::secp256k1::{All, Secp256k1};
18+
use bitcoin::util::bip32::Fingerprint;
19+
20+
use hwi::error::Error;
21+
use hwi::types::{HWIChain, HWIDevice};
22+
use hwi::HWIClient;
23+
24+
use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
25+
26+
#[derive(Debug)]
27+
/// Custom Signer for Hardware Wallets
28+
/// This ignores `sign_options` and leaves the decisions upto the hardware wallet.
29+
pub struct HWISigner {
30+
fingerprint: Fingerprint,
31+
client: HWIClient,
32+
}
33+
34+
impl HWISigner {
35+
/// Create a instance from the specified device and chain
36+
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
37+
let client = HWIClient::get_client(device, false, chain)?;
38+
Ok(HWISigner {
39+
fingerprint: device.fingerprint,
40+
client,
41+
})
42+
}
43+
}
44+
45+
impl SignerCommon for HWISigner {
46+
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
47+
SignerId::Fingerprint(self.fingerprint)
48+
}
49+
}
50+
51+
/// This implementation ignores `sign_options`
52+
impl TransactionSigner for HWISigner {
53+
fn sign_transaction(
54+
&self,
55+
psbt: &mut PartiallySignedTransaction,
56+
_sign_options: &crate::SignOptions,
57+
_secp: &crate::wallet::utils::SecpCtx,
58+
) -> Result<(), SignerError> {
59+
psbt.combine(self.client.sign_tx(psbt)?.psbt)
60+
.expect("Failed to combine HW signed psbt with passed PSBT");
61+
Ok(())
62+
}
63+
}

src/wallet/mod.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ pub(crate) mod utils;
4848
#[cfg_attr(docsrs, doc(cfg(feature = "verify")))]
4949
pub mod verify;
5050

51+
#[cfg(feature = "hardware-signer")]
52+
pub mod hardwaresigner;
53+
5154
pub use utils::IsDust;
5255

5356
#[allow(deprecated)]
@@ -1867,8 +1870,9 @@ where
18671870
}
18681871

18691872
/// Return a fake wallet that appears to be funded for testing.
1870-
pub fn get_funded_wallet(
1873+
pub fn get_funded_wallet_with_coinbase(
18711874
descriptor: &str,
1875+
coinbase: bool,
18721876
) -> (Wallet<AnyDatabase>, (String, Option<String>), bitcoin::Txid) {
18731877
let descriptors = testutils!(@descriptors (descriptor));
18741878
let wallet = Wallet::new(
@@ -1882,7 +1886,7 @@ pub fn get_funded_wallet(
18821886
let funding_address_kix = 0;
18831887

18841888
let tx_meta = testutils! {
1885-
@tx ( (@external descriptors, funding_address_kix) => 50_000 ) (@confirmations 1)
1889+
@tx ( (@external descriptors, funding_address_kix) => 50_000 ) (@confirmations 100)
18861890
};
18871891

18881892
wallet
@@ -1902,11 +1906,17 @@ pub fn get_funded_wallet(
19021906
.set_last_index(KeychainKind::External, funding_address_kix)
19031907
.unwrap();
19041908

1905-
let txid = crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, Some(100));
1909+
let txid = crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, Some(100), (@coinbase coinbase));
19061910

19071911
(wallet, descriptors, txid)
19081912
}
19091913

1914+
/// Return a fake wallet that appears to be funded for testing.
1915+
pub fn get_funded_wallet(
1916+
descriptor: &str,
1917+
) -> (Wallet<AnyDatabase>, (String, Option<String>), bitcoin::Txid) {
1918+
get_funded_wallet_with_coinbase(descriptor, false)
1919+
}
19101920
#[cfg(test)]
19111921
pub(crate) mod test {
19121922
use bitcoin::{util::psbt, Network};
@@ -5414,4 +5424,40 @@ pub(crate) mod test {
54145424
// ...and checking that everything is fine
54155425
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
54165426
}
5427+
5428+
#[cfg(feature = "test-hardware-signer")]
5429+
#[test]
5430+
fn test_create_signer() {
5431+
use hwi::{types, HWIClient};
5432+
5433+
use crate::wallet::hardwaresigner::HWISigner;
5434+
5435+
let devices = HWIClient::enumerate().unwrap();
5436+
let client = HWIClient::get_client(
5437+
devices.first().expect(
5438+
"No devices found. Either plug in a hardware wallet, or start a simulator.",
5439+
),
5440+
true,
5441+
types::HWIChain::Regtest,
5442+
)
5443+
.unwrap();
5444+
let descriptors = client.get_descriptors(None).unwrap();
5445+
let custom_signer =
5446+
HWISigner::from_device(devices.first().unwrap(), types::HWIChain::Regtest).unwrap();
5447+
5448+
let (mut wallet, _, _) = get_funded_wallet_with_coinbase(&descriptors.internal[0], true);
5449+
wallet.add_signer(
5450+
KeychainKind::External,
5451+
SignerOrdering(200),
5452+
Arc::new(custom_signer),
5453+
);
5454+
5455+
let addr = wallet.get_address(LastUnused).unwrap();
5456+
let mut builder = wallet.build_tx();
5457+
builder.drain_to(addr.script_pubkey()).drain_wallet();
5458+
let (mut psbt, _) = builder.finish().unwrap();
5459+
5460+
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
5461+
assert!(finalized);
5462+
}
54175463
}

src/wallet/signer.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ pub enum SignerError {
159159
InvalidSighash,
160160
/// Error while computing the hash to sign
161161
SighashError(sighash::Error),
162+
/// Error while signing using hardware wallets
163+
#[cfg(feature = "hardware-signer")]
164+
HWIError(hwi::error::Error),
165+
}
166+
167+
#[cfg(feature = "hardware-signer")]
168+
impl From<hwi::error::Error> for SignerError {
169+
fn from(e: hwi::error::Error) -> Self {
170+
SignerError::HWIError(e)
171+
}
162172
}
163173

164174
impl From<sighash::Error> for SignerError {

0 commit comments

Comments
 (0)