Skip to content

Commit 3f3f31a

Browse files
committed
feat(hwi): add hwi sign subcommand
- add signing psbt with hardware wallet
1 parent 01f6c76 commit 3f3f31a

File tree

2 files changed

+148
-61
lines changed

2 files changed

+148
-61
lines changed

src/commands.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ pub enum CliSubCommand {
112112
#[command(flatten)]
113113
wallet_opts: WalletOpts,
114114
},
115+
/// Hardware wallet interface operations.
116+
#[cfg(feature = "hwi")]
117+
Hwi {
118+
#[command(flatten)]
119+
wallet_opts: WalletOpts,
120+
#[clap(subcommand)]
121+
subcommand: HwiSubCommand,
122+
},
115123
}
116124

117125
/// Wallet operation subcommands.
@@ -382,12 +390,6 @@ pub enum OfflineWalletSubCommand {
382390
#[arg(env = "BASE64_PSBT", required = true)]
383391
psbt: Vec<String>,
384392
},
385-
#[cfg(feature = "hwi")]
386-
/// Hardware wallet interface operations.
387-
Hwi {
388-
#[clap(subcommand)]
389-
subcommand: HwiSubCommand,
390-
},
391393
}
392394

393395
/// Wallet subcommands that needs a blockchain backend.
@@ -476,6 +478,11 @@ pub enum HwiSubCommand {
476478
Register,
477479
/// Generate address
478480
Address,
481+
/// Sign PSBT with hardware wallet
482+
Sign {
483+
/// The base64-encoded PSBT to sign
484+
psbt: String,
485+
},
479486
}
480487

481488
/// Subcommands available in REPL mode.

src/handlers.rs

Lines changed: 135 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const NUMS_UNSPENDABLE_KEY_HEX: &str =
9292
/// Execute an offline wallet sub-command
9393
///
9494
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
95-
pub async fn handle_offline_wallet_subcommand(
95+
pub fn handle_offline_wallet_subcommand(
9696
wallet: &mut Wallet,
9797
wallet_opts: &WalletOpts,
9898
cli_opts: &CliOpts,
@@ -586,56 +586,6 @@ pub async fn handle_offline_wallet_subcommand(
586586
&json!({ "psbt": BASE64_STANDARD.encode(final_psbt.serialize()) }),
587587
)?)
588588
}
589-
#[cfg(feature = "hwi")]
590-
Hwi { subcommand } => match subcommand {
591-
HwiSubCommand::Devices => {
592-
let device = crate::utils::connect_to_hardware_wallet(
593-
wallet.network(),
594-
wallet_opts,
595-
Some(wallet),
596-
)
597-
.await?;
598-
let device = if let Some(device) = device {
599-
json!({
600-
"type": device.device_kind().to_string(),
601-
"fingerprint": device.get_master_fingerprint().await?.to_string(),
602-
"model": device.device_kind().to_string(),
603-
})
604-
} else {
605-
json!(null)
606-
};
607-
Ok(json!({ "devices": device }))
608-
}
609-
HwiSubCommand::Register => {
610-
let policy = wallet_opts.ext_descriptor.clone().ok_or_else(|| {
611-
Error::Generic(
612-
"External descriptor required for wallet registration".to_string(),
613-
)
614-
})?;
615-
let wallet_name = wallet_opts.wallet.clone().ok_or_else(|| {
616-
Error::Generic("Wallet name is required for wallet registration".to_string())
617-
})?;
618-
619-
let device = crate::utils::connect_to_hardware_wallet(
620-
wallet.network(),
621-
wallet_opts,
622-
Some(wallet),
623-
)
624-
.await?;
625-
let hmac = if let Some(device) = device {
626-
let hmac = device.register_wallet(&wallet_name, &policy).await?;
627-
hmac.map(|h| h.to_lower_hex_string())
628-
} else {
629-
None
630-
};
631-
//TODO: return status of wallet registration
632-
Ok(json!({ "hmac": hmac }))
633-
}
634-
HwiSubCommand::Address => {
635-
let address = wallet.next_unused_address(KeychainKind::External);
636-
Ok(json!({ "address": address.address }))
637-
}
638-
},
639589
}
640590
}
641591

@@ -1100,6 +1050,138 @@ pub(crate) fn handle_compile_subcommand(
11001050
}
11011051
}
11021052

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

12041286
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
12051287
let result =
1206-
handle_offline_wallet_subcommand(&mut wallet, wallet_opts, &cli_opts, offline_subcommand.clone())
1207-
.await?;
1288+
handle_offline_wallet_subcommand(&mut wallet, wallet_opts, &cli_opts, offline_subcommand.clone());
12081289
wallet.persist(&mut persister)?;
12091290
result
12101291
};
@@ -1218,7 +1299,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
12181299
offline_subcommand.clone(),
12191300
)?
12201301
};
1221-
Ok(result)
1302+
Ok(result?)
12221303
}
12231304
CliSubCommand::Key {
12241305
subcommand: key_subcommand,
@@ -1347,7 +1428,6 @@ async fn respond(
13471428
subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
13481429
} => {
13491430
let value = handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
1350-
.await
13511431
.map_err(|e| e.to_string())?;
13521432
Some(value)
13531433
}

0 commit comments

Comments
 (0)