Skip to content

Commit ddaeb28

Browse files
committed
Add a custom signer for hardware wallets
1 parent 6bae52e commit ddaeb28

File tree

5 files changed

+131
-0
lines changed

5 files changed

+131
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
- Add `descriptor::checksum::get_checksum_bytes` method.
99
- Add `Excess` enum to handle remaining amount after coin selection.
1010
- Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`.
11+
- Add a custom Signer for Hardware Wallets.
1112

1213
## [v0.20.0] - [v0.19.0]
1314

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 = { git = "https://github.com/bitcoindevkit/rust-hwi.git", 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/hardwaresigner/mod.rs

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! Hardware wallets
2+
//!
3+
//! This module contains modules required for working with Hardware Wallets
4+
5+
pub mod signer;
6+
7+
#[cfg(test)]
8+
mod tests {
9+
use std::{str::FromStr, sync::Arc};
10+
11+
use bitcoin::{Address, Network};
12+
use hwi::{types, HWIClient};
13+
14+
use crate::{
15+
database::MemoryDatabase, hardwaresigner::signer::HWISigner, signer::SignerOrdering,
16+
KeychainKind, SignOptions, Wallet,
17+
};
18+
19+
fn get_first_device() -> HWIClient {
20+
HWIClient::get_client(
21+
HWIClient::enumerate().unwrap().first().expect(
22+
"No devices found. Either plug in a hardware wallet, or start a simulator.",
23+
),
24+
true,
25+
types::HWIChain::Test,
26+
)
27+
.unwrap()
28+
}
29+
30+
#[test]
31+
fn test_create_signer() {
32+
let descriptors = get_first_device().get_descriptors(None).unwrap();
33+
let devices = HWIClient::enumerate().unwrap();
34+
let custom_signer =
35+
HWISigner::from_device(devices.first().unwrap(), types::HWIChain::Test).unwrap();
36+
37+
let mut wallet = Wallet::new(
38+
&descriptors.internal[0],
39+
Some(&descriptors.receive[0]),
40+
Network::Testnet,
41+
MemoryDatabase::default(),
42+
)
43+
.unwrap();
44+
wallet.add_signer(
45+
KeychainKind::External,
46+
SignerOrdering(200),
47+
Arc::new(custom_signer),
48+
);
49+
50+
let client =
51+
crate::electrum_client::Client::new("ssl://electrum.blockstream.info:60002").unwrap();
52+
let blockchain = crate::blockchain::ElectrumBlockchain::from(client);
53+
wallet
54+
.sync(&blockchain, crate::SyncOptions::default())
55+
.unwrap();
56+
57+
let balance = wallet.get_balance().unwrap();
58+
let faucet_address = Address::from_str("mv4rnyY3Su5gjcDNzbMLKBQkBicCtHUtFB").unwrap();
59+
60+
let mut tx_builder = wallet.build_tx();
61+
tx_builder
62+
.add_recipient(faucet_address.script_pubkey(), balance / 2)
63+
.enable_rbf();
64+
let (mut psbt, _tx_details) = tx_builder.finish().unwrap();
65+
let finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
66+
67+
assert!(finalized, "Tx has not been finalized");
68+
}
69+
}

src/hardwaresigner/signer.rs

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//! HWI Signer
2+
//!
3+
//! This module contains a 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+
_secp: &Secp256k1<All>,
47+
) -> Result<(), SignerError> {
48+
psbt.combine(
49+
self.client
50+
.sign_tx(psbt)
51+
.expect("Hardware Wallet couldn't sign transaction")
52+
.psbt,
53+
)
54+
.expect("Failed to combine HW signed psbt with passed PSBT");
55+
Ok(())
56+
}
57+
}

src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ pub mod database;
265265
pub mod descriptor;
266266
#[cfg(feature = "test-md-docs")]
267267
mod doctest;
268+
#[cfg(feature = "hardware-signer")]
269+
pub mod hardwaresigner;
268270
pub mod keys;
269271
pub(crate) mod psbt;
270272
pub(crate) mod types;

0 commit comments

Comments
 (0)