Skip to content

Commit cd8c5ec

Browse files
committed
Add a custom signer for hardware wallets
1 parent 03d3c78 commit cd8c5ec

File tree

6 files changed

+121
-1
lines changed

6 files changed

+121
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Change the meaning of the `fee_amount` field inside `CoinSelectionResult`: from now on the `fee_amount` will represent only the fees asociated with the utxos in the `selected` field of `CoinSelectionResult`.
1616
- New `RpcBlockchain` implementation with various fixes.
1717
- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`.
18+
- Add a custom Signer for Hardware Wallets.
1819

1920
## [v0.20.0] - [v0.19.0]
2021

Cargo.toml

+2
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.1", 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

src/wallet/hardwaresigner/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//! Hardware wallets
2+
//!
3+
//! This module contains modules required for working with Hardware Wallets.
4+
//! This module can contain various vendor specific code, if required.
5+
6+
pub mod signer;

src/wallet/hardwaresigner/signer.rs

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! HWI Signer
2+
//!
3+
//! This module contains a simple implementation of a Custom Signer for rust-hwi
4+
5+
use bitcoin::{
6+
psbt::PartiallySignedTransaction,
7+
secp256k1::{All, Secp256k1},
8+
util::bip32::Fingerprint,
9+
};
10+
use hwi::{
11+
error::Error,
12+
types::{HWIChain, HWIDevice},
13+
HWIClient,
14+
};
15+
16+
use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
17+
18+
#[derive(Debug)]
19+
/// Custom Signer for Hardware Wallets
20+
pub struct HWISigner {
21+
fingerprint: Fingerprint,
22+
client: HWIClient,
23+
}
24+
25+
impl HWISigner {
26+
/// Create a instance from the specified device and chain
27+
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
28+
let client = HWIClient::get_client(device, false, chain)?;
29+
Ok(HWISigner {
30+
fingerprint: device.fingerprint,
31+
client,
32+
})
33+
}
34+
}
35+
36+
impl SignerCommon for HWISigner {
37+
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
38+
SignerId::Fingerprint(self.fingerprint)
39+
}
40+
}
41+
42+
impl TransactionSigner for HWISigner {
43+
fn sign_transaction(
44+
&self,
45+
psbt: &mut PartiallySignedTransaction,
46+
_sign_options: &crate::SignOptions,
47+
_secp: &crate::wallet::utils::SecpCtx,
48+
) -> Result<(), SignerError> {
49+
psbt.combine(self.client.sign_tx(psbt)?.psbt)
50+
.expect("Failed to combine HW signed psbt with passed PSBT");
51+
Ok(())
52+
}
53+
}

src/wallet/mod.rs

+48
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)]
@@ -5414,4 +5417,49 @@ pub(crate) mod test {
54145417
// ...and checking that everything is fine
54155418
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
54165419
}
5420+
5421+
#[cfg(feature = "hardware-signer")]
5422+
#[test]
5423+
fn test_create_signer() {
5424+
use bitcoin::util::bip32::DerivationPath;
5425+
use hwi::{types, HWIClient};
5426+
5427+
use crate::wallet::hardwaresigner::signer::HWISigner;
5428+
5429+
let devices = HWIClient::enumerate().unwrap();
5430+
let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap();
5431+
let client = HWIClient::get_client(
5432+
devices.first().expect(
5433+
"No devices found. Either plug in a hardware wallet, or start a simulator.",
5434+
),
5435+
true,
5436+
types::HWIChain::Regtest,
5437+
)
5438+
.unwrap();
5439+
let descriptors = client.get_descriptors(None).unwrap();
5440+
let custom_signer =
5441+
HWISigner::from_device(devices.first().unwrap(), types::HWIChain::Regtest).unwrap();
5442+
5443+
let (mut wallet, _, _) = get_funded_wallet(&descriptors.internal[0]);
5444+
wallet.add_signer(
5445+
KeychainKind::External,
5446+
SignerOrdering(200),
5447+
Arc::new(custom_signer),
5448+
);
5449+
5450+
let addr = client
5451+
.display_address_with_path(&derivation_path, types::HWIAddressType::Legacy)
5452+
.unwrap();
5453+
let mut builder = wallet.build_tx();
5454+
builder
5455+
.drain_to(addr.address.script_pubkey())
5456+
.drain_wallet();
5457+
let (mut psbt, _) = builder.finish().unwrap();
5458+
5459+
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
5460+
assert!(finalized);
5461+
5462+
// let extracted = psbt.extract_tx();
5463+
// assert_eq!(extracted.input[0].witness.len(), 2);
5464+
}
54175465
}

src/wallet/signer.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ impl From<Fingerprint> for SignerId {
129129
}
130130

131131
/// Signing error
132-
#[derive(Debug, PartialEq, Eq, Clone)]
132+
#[derive(Debug)]
133133
pub enum SignerError {
134134
/// The private key is missing for the required public key
135135
MissingKey,
@@ -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)