Skip to content

Commit 7aee2d1

Browse files
committed
feat(hwi): add hwi sign subcommand
- add signing psbt with hardware wallet
1 parent 9b70524 commit 7aee2d1

File tree

4 files changed

+175
-69
lines changed

4 files changed

+175
-69
lines changed

Cargo.lock

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

src/commands.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ pub enum CliSubCommand {
127127
/// Optional key: xprv, xpub, or mnemonic phrase
128128
key: Option<String>,
129129
},
130+
/// Hardware wallet interface operations.
131+
#[cfg(feature = "hwi")]
132+
Hwi {
133+
#[command(flatten)]
134+
wallet_opts: WalletOpts,
135+
#[clap(subcommand)]
136+
subcommand: HwiSubCommand,
137+
},
130138
}
131139
/// Wallet operation subcommands.
132140
#[derive(Debug, Subcommand, Clone, PartialEq)]
@@ -396,12 +404,6 @@ pub enum OfflineWalletSubCommand {
396404
#[arg(env = "BASE64_PSBT", required = true)]
397405
psbt: Vec<String>,
398406
},
399-
#[cfg(feature = "hwi")]
400-
/// Hardware wallet interface operations.
401-
Hwi {
402-
#[clap(subcommand)]
403-
subcommand: HwiSubCommand,
404-
},
405407
}
406408

407409
/// Wallet subcommands that needs a blockchain backend.
@@ -490,6 +492,11 @@ pub enum HwiSubCommand {
490492
Register,
491493
/// Generate address
492494
Address,
495+
/// Sign PSBT with hardware wallet
496+
Sign {
497+
/// The base64-encoded PSBT to sign
498+
psbt: String,
499+
},
493500
}
494501

495502
/// Subcommands available in REPL mode.

src/handlers.rs

Lines changed: 145 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use bdk_redb::Store as RedbStore;
2121
use bdk_wallet::bip39::{Language, Mnemonic};
2222
use bdk_wallet::bitcoin::base64::Engine;
2323
use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD;
24+
#[cfg(feature = "hwi")]
25+
use bdk_wallet::bitcoin::hex::DisplayHex;
2426
use bdk_wallet::bitcoin::{
2527
Address, Amount, FeeRate, Network, Psbt, Sequence, Txid,
2628
bip32::{DerivationPath, KeySource},
@@ -29,8 +31,6 @@ use bdk_wallet::bitcoin::{
2931
secp256k1::Secp256k1,
3032
};
3133
use bdk_wallet::chain::ChainPosition;
32-
#[cfg(feature = "hwi")]
33-
use bdk_wallet::bitcoin::hex::DisplayHex;
3434
use bdk_wallet::descriptor::Segwitv0;
3535
use bdk_wallet::keys::{
3636
DerivableKey, DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratableKey, GeneratedKey,
@@ -88,7 +88,7 @@ const NUMS_UNSPENDABLE_KEY_HEX: &str =
8888
/// Execute an offline wallet sub-command
8989
///
9090
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
91-
pub async fn handle_offline_wallet_subcommand(
91+
pub fn handle_offline_wallet_subcommand(
9292
wallet: &mut Wallet,
9393
wallet_opts: &WalletOpts,
9494
cli_opts: &CliOpts,
@@ -582,56 +582,6 @@ pub async fn handle_offline_wallet_subcommand(
582582
&json!({ "psbt": BASE64_STANDARD.encode(final_psbt.serialize()) }),
583583
)?)
584584
}
585-
#[cfg(feature = "hwi")]
586-
Hwi { subcommand } => match subcommand {
587-
HwiSubCommand::Devices => {
588-
let device = crate::utils::connect_to_hardware_wallet(
589-
wallet.network(),
590-
wallet_opts,
591-
Some(wallet),
592-
)
593-
.await?;
594-
let device = if let Some(device) = device {
595-
json!({
596-
"type": device.device_kind().to_string(),
597-
"fingerprint": device.get_master_fingerprint().await?.to_string(),
598-
"model": device.device_kind().to_string(),
599-
})
600-
} else {
601-
json!(null)
602-
};
603-
Ok(json!({ "devices": device }))
604-
}
605-
HwiSubCommand::Register => {
606-
let policy = wallet_opts.ext_descriptor.clone().ok_or_else(|| {
607-
Error::Generic(
608-
"External descriptor required for wallet registration".to_string(),
609-
)
610-
})?;
611-
let wallet_name = wallet_opts.wallet.clone().ok_or_else(|| {
612-
Error::Generic("Wallet name is required for wallet registration".to_string())
613-
})?;
614-
615-
let device = crate::utils::connect_to_hardware_wallet(
616-
wallet.network(),
617-
wallet_opts,
618-
Some(wallet),
619-
)
620-
.await?;
621-
let hmac = if let Some(device) = device {
622-
let hmac = device.register_wallet(&wallet_name, &policy).await?;
623-
hmac.map(|h| h.to_lower_hex_string())
624-
} else {
625-
None
626-
};
627-
//TODO: return status of wallet registration
628-
Ok(json!({ "hmac": hmac }))
629-
}
630-
HwiSubCommand::Address => {
631-
let address = wallet.next_unused_address(KeychainKind::External);
632-
Ok(json!({ "address": address.address }))
633-
}
634-
},
635585
}
636586
}
637587

@@ -1096,6 +1046,138 @@ pub(crate) fn handle_compile_subcommand(
10961046
}
10971047
}
10981048

1049+
/// Handle hardware wallet operations
1050+
#[cfg(feature = "hwi")]
1051+
pub async fn handle_hwi_subcommand(
1052+
network: Network,
1053+
wallet_opts: &WalletOpts,
1054+
subcommand: HwiSubCommand,
1055+
) -> Result<serde_json::Value, Error> {
1056+
match subcommand {
1057+
HwiSubCommand::Devices => {
1058+
let devices = crate::utils::connect_to_hardware_wallet(
1059+
wallet.network(),
1060+
wallet_opts,
1061+
Some(wallet),
1062+
)
1063+
.await?;
1064+
let device = if let Some(device) = device {
1065+
json!({
1066+
"type": device.device_kind().to_string(),
1067+
"fingerprint": device.get_master_fingerprint().await?.to_string(),
1068+
"model": device.device_kind().to_string(),
1069+
})
1070+
} else {
1071+
json!(null)
1072+
};
1073+
Ok(json!({ "devices": device }))
1074+
}
1075+
HwiSubCommand::Register => {
1076+
let policy = wallet_opts.ext_descriptor.clone().ok_or_else(|| {
1077+
Error::Generic("External descriptor required for wallet registration".to_string())
1078+
})?;
1079+
let wallet_name = wallet_opts.wallet.clone().ok_or_else(|| {
1080+
Error::Generic("Wallet name is required for wallet registration".to_string())
1081+
})?;
1082+
1083+
let home_dir = prepare_home_dir(None)?;
1084+
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
1085+
#[cfg(feature = "sqlite")]
1086+
let wallet = {
1087+
let mut persister = match &wallet_opts.database_type {
1088+
DatabaseType::Sqlite => {
1089+
let db_file = database_path.join("wallet.sqlite");
1090+
let connection = Connection::open(db_file)?;
1091+
log::debug!("Sqlite database opened successfully");
1092+
connection
1093+
}
1094+
};
1095+
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
1096+
wallet.persist(&mut persister)?;
1097+
wallet
1098+
};
1099+
#[cfg(not(feature = "sqlite"))]
1100+
let wallet = new_wallet(network, wallet_opts)?;
1101+
1102+
let device = crate::utils::connect_to_hardware_wallet(
1103+
wallet.network(),
1104+
wallet_opts,
1105+
Some(wallet),
1106+
)
1107+
.await?;
1108+
let hmac = if let Some(device) = device {
1109+
let hmac = device.register_wallet(&wallet_name, &policy).await?;
1110+
hmac.map(|h| h.to_lower_hex_string())
1111+
} else {
1112+
None
1113+
};
1114+
Ok(json!({ "hmac": hmac }))
1115+
}
1116+
HwiSubCommand::Address => {
1117+
let home_dir = prepare_home_dir(None)?;
1118+
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
1119+
#[cfg(feature = "sqlite")]
1120+
let wallet = {
1121+
let mut persister = match &wallet_opts.database_type {
1122+
DatabaseType::Sqlite => {
1123+
let db_file = database_path.join("wallet.sqlite");
1124+
let connection = Connection::open(db_file)?;
1125+
log::debug!("Sqlite database opened successfully");
1126+
connection
1127+
}
1128+
};
1129+
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
1130+
wallet.persist(&mut persister)?;
1131+
wallet
1132+
};
1133+
#[cfg(not(feature = "sqlite"))]
1134+
let wallet = new_wallet(network, wallet_opts)?;
1135+
1136+
let address = wallet.next_unused_address(KeychainKind::External);
1137+
Ok(json!({ "address": address.address }))
1138+
}
1139+
HwiSubCommand::Sign { psbt } => {
1140+
let home_dir = prepare_home_dir(None)?;
1141+
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
1142+
#[cfg(feature = "sqlite")]
1143+
let wallet = {
1144+
let mut persister = match &wallet_opts.database_type {
1145+
DatabaseType::Sqlite => {
1146+
let db_file = database_path.join("wallet.sqlite");
1147+
let connection = Connection::open(db_file)?;
1148+
log::debug!("Sqlite database opened successfully");
1149+
connection
1150+
}
1151+
};
1152+
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
1153+
wallet.persist(&mut persister)?;
1154+
wallet
1155+
};
1156+
#[cfg(not(feature = "sqlite"))]
1157+
let wallet = new_wallet(network, wallet_opts)?;
1158+
1159+
let mut psbt = Psbt::from_str(&psbt)
1160+
.map_err(|e| Error::Generic(format!("Failed to parse PSBT: {e}")))?;
1161+
let device = crate::utils::connect_to_hardware_wallet(
1162+
wallet.network(),
1163+
wallet_opts,
1164+
Some(wallet),
1165+
)
1166+
.await?;
1167+
let signed_psbt = if let Some(device) = device {
1168+
device
1169+
.sign_tx(&mut psbt)
1170+
.await
1171+
.map_err(|e| Error::Generic(format!("Failed to sign PSBT: {e}")))?;
1172+
Some(psbt.to_string())
1173+
} else {
1174+
None
1175+
};
1176+
Ok(json!({ "psbt": signed_psbt }))
1177+
}
1178+
}
1179+
}
1180+
10991181
/// The global top level handler.
11001182
pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
11011183
let network = cli_opts.network;
@@ -1197,9 +1279,12 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
11971279
};
11981280

11991281
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
1200-
let result =
1201-
handle_offline_wallet_subcommand(&mut wallet, wallet_opts, &cli_opts, offline_subcommand.clone())
1202-
.await?;
1282+
let result = handle_offline_wallet_subcommand(
1283+
&mut wallet,
1284+
wallet_opts,
1285+
&cli_opts,
1286+
offline_subcommand.clone(),
1287+
);
12031288
wallet.persist(&mut persister)?;
12041289
result
12051290
};
@@ -1213,7 +1298,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
12131298
offline_subcommand.clone(),
12141299
)?
12151300
};
1216-
Ok(result)
1301+
Ok(result?)
12171302
}
12181303
CliSubCommand::Key {
12191304
subcommand: key_subcommand,
@@ -1345,9 +1430,9 @@ async fn respond(
13451430
ReplSubCommand::Wallet {
13461431
subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
13471432
} => {
1348-
let value = handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
1349-
.await
1350-
.map_err(|e| e.to_string())?;
1433+
let value =
1434+
handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
1435+
.map_err(|e| e.to_string())?;
13511436
Some(value)
13521437
}
13531438
ReplSubCommand::Key { subcommand } => {

src/utils.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use bdk_kyoto::{
2323
BuilderExt, Info, LightClient, Receiver, ScanType::Sync, UnboundedReceiver, Warning,
2424
builder::Builder,
2525
};
26+
use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf};
2627
use bdk_wallet::{
2728
KeychainKind,
2829
bitcoin::bip32::{DerivationPath, Xpub},
@@ -34,17 +35,16 @@ use bdk_wallet::{
3435
template::DescriptorTemplate,
3536
};
3637
use cli_table::{Cell, CellStruct, Style, Table};
37-
use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf};
3838
#[cfg(feature = "hwi")]
3939
use {
4040
async_hwi::jade::{self, Jade},
4141
async_hwi::ledger::{HidApi, LedgerSimulator},
4242
async_hwi::specter::{Specter, SpecterSimulator},
43+
async_hwi::{HWI, coldcard},
4344
async_hwi::{
4445
bitbox::api::runtime,
4546
bitbox::{BitBox02, PairingBitbox02WithLocalCache},
4647
},
47-
async_hwi::{coldcard, HWI},
4848
};
4949

5050
#[cfg(any(
@@ -644,7 +644,7 @@ pub fn format_descriptor_output(result: &Value, pretty: bool) -> Result<String,
644644
Ok(format!("{table}"))
645645
}
646646

647-
#[cfg(feature="hwi")]
647+
#[cfg(feature = "hwi")]
648648
pub async fn connect_to_hardware_wallet(
649649
network: Network,
650650
wallet_opts: &WalletOpts,

0 commit comments

Comments
 (0)