Skip to content

Commit 91892b7

Browse files
committed
feat(hwi): set hwi as top level command
- add ledger and coldcard integration - update hwi as top level command - update CHANGELOG
1 parent 0cc925a commit 91892b7

File tree

5 files changed

+131
-117
lines changed

5 files changed

+131
-117
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
77

88
- Removed MSRV and bumped Rust Edition to 2024
99
- Add `--pretty` top level flag for formatting commands output in a tabular format
10+
- Add `hwi` top level command with subcommands: `devices`, `register`, `address` and `sign` transaction
1011

1112
## [1.0.0]
1213

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ pub enum CliSubCommand {
116116
#[cfg(feature = "hwi")]
117117
Hwi {
118118
#[command(flatten)]
119-
wallet_opts: WalletOpts,
119+
hwi_opts: HwiOpts,
120120
#[clap(subcommand)]
121121
subcommand: HwiSubCommand,
122122
},
@@ -227,6 +227,23 @@ pub struct WalletOpts {
227227
pub compactfilter_opts: CompactFilterOpts,
228228
}
229229

230+
/// HWI specific options
231+
#[cfg(feature = "hwi")]
232+
#[derive(Clone, Debug, PartialEq, Eq, Args)]
233+
pub struct HwiOpts {
234+
/// Wallet name
235+
#[arg(env = "WALLET", short = 'w', long = "wallet")]
236+
pub wallet: Option<String>,
237+
238+
/// External descriptor
239+
#[arg(env = "EXT_DESCRIPTOR", short = 'e', long = "ext_descriptor")]
240+
pub ext_descriptor: Option<String>,
241+
242+
/// Database type
243+
#[arg(short = 'd', long = "database_type")]
244+
pub database_type: Option<DatabaseType>,
245+
}
246+
230247
/// Options to configure a SOCKS5 proxy for a blockchain client connection.
231248
#[cfg(any(feature = "electrum", feature = "esplora"))]
232249
#[derive(Debug, Args, Clone, PartialEq, Eq)]

src/handlers.rs

Lines changed: 67 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,20 +1047,14 @@ pub(crate) fn handle_compile_subcommand(
10471047
#[cfg(feature = "hwi")]
10481048
pub async fn handle_hwi_subcommand(
10491049
network: Network,
1050-
wallet_opts: &WalletOpts,
1050+
hwi_opts: &HwiOpts,
10511051
subcommand: HwiSubCommand,
10521052
) -> Result<serde_json::Value, Error> {
10531053
match subcommand {
10541054
HwiSubCommand::Devices => {
1055-
let devices = crate::utils::connect_to_hardware_wallet(
1056-
wallet.network(),
1057-
wallet_opts,
1058-
Some(wallet),
1059-
)
1060-
.await?;
1061-
let device = if let Some(device) = device {
1055+
let devices = crate::utils::connect_to_hardware_wallet(network, hwi_opts).await?;
1056+
let device = if let Some(device) = devices {
10621057
json!({
1063-
"type": device.device_kind().to_string(),
10641058
"fingerprint": device.get_master_fingerprint().await?.to_string(),
10651059
"model": device.device_kind().to_string(),
10661060
})
@@ -1070,97 +1064,82 @@ pub async fn handle_hwi_subcommand(
10701064
Ok(json!({ "devices": device }))
10711065
}
10721066
HwiSubCommand::Register => {
1073-
let policy = wallet_opts.ext_descriptor.clone().ok_or_else(|| {
1067+
let policy = hwi_opts.ext_descriptor.clone().ok_or_else(|| {
10741068
Error::Generic("External descriptor required for wallet registration".to_string())
10751069
})?;
1076-
let wallet_name = wallet_opts.wallet.clone().ok_or_else(|| {
1070+
let wallet_name = hwi_opts.wallet.clone().ok_or_else(|| {
10771071
Error::Generic("Wallet name is required for wallet registration".to_string())
10781072
})?;
10791073

1080-
let home_dir = prepare_home_dir(None)?;
1081-
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
1082-
#[cfg(feature = "sqlite")]
1083-
let wallet = {
1084-
let mut persister = match &wallet_opts.database_type {
1085-
DatabaseType::Sqlite => {
1086-
let db_file = database_path.join("wallet.sqlite");
1087-
let connection = Connection::open(db_file)?;
1088-
log::debug!("Sqlite database opened successfully");
1089-
connection
1074+
let device = crate::utils::connect_to_hardware_wallet(network, hwi_opts).await?;
1075+
1076+
match device {
1077+
None => Ok(json!({
1078+
"success": false,
1079+
"error": "No hardware wallet detected"
1080+
})),
1081+
Some(device) => match device.register_wallet(&wallet_name, &policy).await {
1082+
Ok(hmac_opt) => {
1083+
let hmac_hex = hmac_opt.map(|h| {
1084+
let bytes: &[u8] = &h;
1085+
bytes.to_lower_hex_string()
1086+
});
1087+
Ok(json!({
1088+
"success": true,
1089+
"hmac": hmac_hex
1090+
}))
10901091
}
1091-
};
1092-
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
1093-
wallet.persist(&mut persister)?;
1094-
wallet
1095-
};
1096-
#[cfg(not(feature = "sqlite"))]
1097-
let wallet = new_wallet(network, wallet_opts)?;
1098-
1099-
let device = crate::utils::connect_to_hardware_wallet(
1100-
wallet.network(),
1101-
wallet_opts,
1102-
Some(wallet),
1103-
)
1104-
.await?;
1105-
let hmac = if let Some(device) = device {
1106-
let hmac = device.register_wallet(&wallet_name, &policy).await?;
1107-
hmac.map(|h| h.to_lower_hex_string())
1108-
} else {
1109-
None
1110-
};
1111-
Ok(json!({ "hmac": hmac }))
1092+
Err(e) => Err(Error::Generic(format!("Wallet registration failed: {e}"))),
1093+
},
1094+
}
11121095
}
11131096
HwiSubCommand::Address => {
1097+
let ext_descriptor = hwi_opts.ext_descriptor.clone().ok_or_else(|| {
1098+
Error::Generic("External descriptor required for address generation".to_string())
1099+
})?;
1100+
let wallet_name = hwi_opts.wallet.clone().ok_or_else(|| {
1101+
Error::Generic("Wallet name is required for address generation".to_string())
1102+
})?;
1103+
1104+
let database = hwi_opts.database_type.clone().ok_or_else(|| {
1105+
Error::Generic("Database type is required for address generation".to_string())
1106+
})?;
1107+
11141108
let home_dir = prepare_home_dir(None)?;
1115-
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
1109+
let database_path = prepare_wallet_db_dir(&Some(wallet_name.clone()), &home_dir)?;
1110+
1111+
let wallet_opts = WalletOpts {
1112+
wallet: Some(wallet_name),
1113+
verbose: false,
1114+
ext_descriptor: Some(ext_descriptor),
1115+
int_descriptor: None,
1116+
#[cfg(feature = "sqlite")]
1117+
database_type: database,
1118+
};
1119+
11161120
#[cfg(feature = "sqlite")]
1117-
let wallet = {
1118-
let mut persister = match &wallet_opts.database_type {
1119-
DatabaseType::Sqlite => {
1120-
let db_file = database_path.join("wallet.sqlite");
1121-
let connection = Connection::open(db_file)?;
1122-
log::debug!("Sqlite database opened successfully");
1123-
connection
1124-
}
1125-
};
1126-
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
1121+
let mut wallet = if hwi_opts.database_type.is_some() {
1122+
let db_file = database_path.join("wallet.sqlite");
1123+
let mut persister = Connection::open(db_file)?;
1124+
let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
11271125
wallet.persist(&mut persister)?;
11281126
wallet
1127+
} else {
1128+
return Err(Error::Generic(
1129+
"Could not connect to sqlite database".to_string(),
1130+
));
11291131
};
1132+
11301133
#[cfg(not(feature = "sqlite"))]
1131-
let wallet = new_wallet(network, wallet_opts)?;
1134+
let mut wallet = new_wallet(network, &wallet_opts)?;
11321135

11331136
let address = wallet.next_unused_address(KeychainKind::External);
11341137
Ok(json!({ "address": address.address }))
11351138
}
11361139
HwiSubCommand::Sign { psbt } => {
1137-
let home_dir = prepare_home_dir(None)?;
1138-
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
1139-
#[cfg(feature = "sqlite")]
1140-
let wallet = {
1141-
let mut persister = match &wallet_opts.database_type {
1142-
DatabaseType::Sqlite => {
1143-
let db_file = database_path.join("wallet.sqlite");
1144-
let connection = Connection::open(db_file)?;
1145-
log::debug!("Sqlite database opened successfully");
1146-
connection
1147-
}
1148-
};
1149-
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
1150-
wallet.persist(&mut persister)?;
1151-
wallet
1152-
};
1153-
#[cfg(not(feature = "sqlite"))]
1154-
let wallet = new_wallet(network, wallet_opts)?;
1155-
11561140
let mut psbt = Psbt::from_str(&psbt)
11571141
.map_err(|e| Error::Generic(format!("Failed to parse PSBT: {e}")))?;
1158-
let device = crate::utils::connect_to_hardware_wallet(
1159-
wallet.network(),
1160-
wallet_opts,
1161-
Some(wallet),
1162-
)
1163-
.await?;
1142+
let device = crate::utils::connect_to_hardware_wallet(network, hwi_opts).await?;
11641143
let signed_psbt = if let Some(device) = device {
11651144
device
11661145
.sign_tx(&mut psbt)
@@ -1383,6 +1362,15 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
13831362
}
13841363
Ok("".to_string())
13851364
}
1365+
1366+
#[cfg(feature = "hwi")]
1367+
CliSubCommand::Hwi {
1368+
hwi_opts,
1369+
subcommand,
1370+
} => {
1371+
let result = handle_hwi_subcommand(network, &hwi_opts, subcommand).await?;
1372+
Ok(serde_json::to_string_pretty(&result).map_err(|e| Error::SerdeJson(e))?)
1373+
}
13861374
};
13871375
result
13881376
}

src/utils.rs

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ use bdk_kyoto::{
2626
use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf};
2727
#[cfg(feature = "hwi")]
2828
use {
29+
crate::commands::HwiOpts,
2930
async_hwi::jade::{self, Jade},
30-
async_hwi::ledger::{HidApi, LedgerSimulator},
31+
async_hwi::ledger::{HidApi, Ledger, LedgerSimulator, TransportHID},
3132
async_hwi::specter::{Specter, SpecterSimulator},
3233
async_hwi::{
3334
bitbox::api::runtime,
@@ -394,14 +395,8 @@ pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String {
394395
#[cfg(feature="hwi")]
395396
pub async fn connect_to_hardware_wallet(
396397
network: Network,
397-
wallet_opts: &WalletOpts,
398-
wallet: Option<&Wallet>,
398+
hwi_opts: &HwiOpts,
399399
) -> Result<Option<Box<dyn HWI + Send>>, Error> {
400-
let wallet_name = &wallet_opts.wallet;
401-
let ext_descriptor = &wallet_opts.ext_descriptor;
402-
403-
// TODO: add Ledger implementation
404-
405400
if let Ok(device) = SpecterSimulator::try_connect().await {
406401
return Ok(Some(device.into()));
407402
}
@@ -414,7 +409,7 @@ pub async fn connect_to_hardware_wallet(
414409

415410
match Jade::enumerate().await {
416411
Err(e) => {
417-
println!("Jade enumeration error: {:?}", e);
412+
println!("Jade enumeration error: {e:?}");
418413
}
419414
Ok(devices) => {
420415
for device in devices {
@@ -436,28 +431,32 @@ pub async fn connect_to_hardware_wallet(
436431
return Ok(Some(device.into()));
437432
}
438433

439-
let api = Box::new(
440-
HidApi::new().map_err(|e| Error::HwiError(async_hwi::Error::Device(e.to_string())))?,
441-
);
434+
let api = Box::new(HidApi::new().map_err(|e| Error::Generic(e.to_string()))?);
442435

443436
for device_info in api.device_list() {
437+
let ext_descriptor = hwi_opts.ext_descriptor.clone().ok_or_else(|| {
438+
Error::Generic("External descriptor is required for connecting to bitbox and coldcard hardware devices".to_string())
439+
})?;
440+
let wallet_name = hwi_opts.wallet.clone().ok_or_else(|| {
441+
Error::Generic(
442+
"Wallet name is required for connecting to bitbox and coldcard hardware devices"
443+
.to_string(),
444+
)
445+
})?;
444446
if async_hwi::bitbox::is_bitbox02(device_info) {
445447
if let Ok(device) = device_info
446448
.open_device(&api)
447-
.map_err(|e| Error::HwiError(async_hwi::Error::Device(e.to_string())))
449+
.map_err(|e| Error::Generic(e.to_string()))
448450
{
449451
if let Ok(device) =
450452
PairingBitbox02WithLocalCache::<runtime::TokioRuntime>::connect(device, None)
451453
.await
452454
{
453455
if let Ok((device, _)) = device.wait_confirm().await {
454456
let mut bb02 = BitBox02::from(device).with_network(network);
455-
if let Some(_wallet) = wallet.as_ref() {
456-
if let Some(policy) = ext_descriptor {
457-
bb02 =
458-
bb02.with_policy(policy.as_str()).map_err(Error::HwiError)?;
459-
}
460-
}
457+
bb02 = bb02
458+
.with_policy(&ext_descriptor.as_str())
459+
.map_err(Error::HwiError)?;
461460
return Ok(Some(bb02.into()));
462461
}
463462
}
@@ -468,26 +467,35 @@ pub async fn connect_to_hardware_wallet(
468467
{
469468
if let Some(sn) = device_info.serial_number() {
470469
if let Ok((cc, _)) = coldcard::api::Coldcard::open(&api, sn, None)
471-
.map_err(|e| Error::HwiError(async_hwi::Error::Device(e.to_string())))
470+
.map_err(|e| Error::Generic(e.to_string()))
472471
{
473472
let mut hw = coldcard::Coldcard::from(cc);
474-
if let Some(_wallet) = wallet {
475-
hw = hw.with_wallet_name(
476-
wallet_name
477-
.clone()
478-
.ok_or_else(|| {
479-
Error::HwiError(async_hwi::Error::Device(
480-
"coldcard requires a wallet name".into(),
481-
))
482-
})?
483-
.to_string(),
484-
);
485-
}
473+
hw = hw.with_wallet_name(wallet_name.to_string());
486474
return Ok(Some(hw.into()));
487475
}
488476
}
489477
}
490478
}
491479

480+
for detected in Ledger::<TransportHID>::enumerate(&api) {
481+
let ext_descriptor = hwi_opts.ext_descriptor.clone().ok_or_else(|| {
482+
Error::Generic(
483+
"External descriptor required for connecting to ledger hardware device".to_string(),
484+
)
485+
})?;
486+
let wallet_name = hwi_opts.wallet.clone().ok_or_else(|| {
487+
Error::Generic(
488+
"Wallet name is required for connecting to ledger hardware device".to_string(),
489+
)
490+
})?;
491+
492+
if let Ok(mut device) = Ledger::<TransportHID>::connect(&api, detected) {
493+
device = device
494+
.with_wallet(wallet_name, &ext_descriptor, None)
495+
.map_err(Error::HwiError)?;
496+
return Ok(Some(device.into()));
497+
}
498+
}
499+
492500
Ok(None)
493501
}

0 commit comments

Comments
 (0)