Skip to content

Commit 2917013

Browse files
committed
feat(hwi): add hwi sign subcommand
- add signing psbt with hardware wallet
1 parent 0503d5f commit 2917013

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
@@ -109,6 +109,14 @@ pub enum CliSubCommand {
109109
#[command(flatten)]
110110
wallet_opts: WalletOpts,
111111
},
112+
/// Hardware wallet interface operations.
113+
#[cfg(feature = "hwi")]
114+
Hwi {
115+
#[command(flatten)]
116+
wallet_opts: WalletOpts,
117+
#[clap(subcommand)]
118+
subcommand: HwiSubCommand,
119+
},
112120
}
113121

114122
/// Wallet operation subcommands.
@@ -380,12 +388,6 @@ pub enum OfflineWalletSubCommand {
380388
#[arg(env = "BASE64_PSBT", required = true)]
381389
psbt: Vec<String>,
382390
},
383-
#[cfg(feature = "hwi")]
384-
/// Hardware wallet interface operations.
385-
Hwi {
386-
#[clap(subcommand)]
387-
subcommand: HwiSubCommand,
388-
},
389391
}
390392

391393
/// Wallet subcommands that needs a blockchain backend.
@@ -474,6 +476,11 @@ pub enum HwiSubCommand {
474476
Register,
475477
/// Generate address
476478
Address,
479+
/// Sign PSBT with hardware wallet
480+
Sign {
481+
/// The base64-encoded PSBT to sign
482+
psbt: String,
483+
},
477484
}
478485

479486
/// Subcommands available in REPL mode.

src/handlers.rs

Lines changed: 135 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ use {
7777
/// Execute an offline wallet sub-command
7878
///
7979
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
80-
pub async fn handle_offline_wallet_subcommand(
80+
pub fn handle_offline_wallet_subcommand(
8181
wallet: &mut Wallet,
8282
wallet_opts: &WalletOpts,
8383
offline_subcommand: OfflineWalletSubCommand,
@@ -330,56 +330,6 @@ pub async fn handle_offline_wallet_subcommand(
330330
)?;
331331
Ok(json!({ "psbt": BASE64_STANDARD.encode(final_psbt.serialize()) }))
332332
}
333-
#[cfg(feature = "hwi")]
334-
Hwi { subcommand } => match subcommand {
335-
HwiSubCommand::Devices => {
336-
let device = crate::utils::connect_to_hardware_wallet(
337-
wallet.network(),
338-
wallet_opts,
339-
Some(wallet),
340-
)
341-
.await?;
342-
let device = if let Some(device) = device {
343-
json!({
344-
"type": device.device_kind().to_string(),
345-
"fingerprint": device.get_master_fingerprint().await?.to_string(),
346-
"model": device.device_kind().to_string(),
347-
})
348-
} else {
349-
json!(null)
350-
};
351-
Ok(json!({ "devices": device }))
352-
}
353-
HwiSubCommand::Register => {
354-
let policy = wallet_opts.ext_descriptor.clone().ok_or_else(|| {
355-
Error::Generic(
356-
"External descriptor required for wallet registration".to_string(),
357-
)
358-
})?;
359-
let wallet_name = wallet_opts.wallet.clone().ok_or_else(|| {
360-
Error::Generic("Wallet name is required for wallet registration".to_string())
361-
})?;
362-
363-
let device = crate::utils::connect_to_hardware_wallet(
364-
wallet.network(),
365-
wallet_opts,
366-
Some(wallet),
367-
)
368-
.await?;
369-
let hmac = if let Some(device) = device {
370-
let hmac = device.register_wallet(&wallet_name, &policy).await?;
371-
hmac.map(|h| h.to_lower_hex_string())
372-
} else {
373-
None
374-
};
375-
//TODO: return status of wallet registration
376-
Ok(json!({ "hmac": hmac }))
377-
}
378-
HwiSubCommand::Address => {
379-
let address = wallet.next_unused_address(KeychainKind::External);
380-
Ok(json!({ "address": address.address }))
381-
}
382-
},
383333
}
384334
}
385335

@@ -793,6 +743,138 @@ pub(crate) fn handle_compile_subcommand(
793743
Ok(json!({"descriptor": descriptor.to_string()}))
794744
}
795745

746+
/// Handle hardware wallet operations
747+
#[cfg(feature = "hwi")]
748+
pub async fn handle_hwi_subcommand(
749+
network: Network,
750+
wallet_opts: &WalletOpts,
751+
subcommand: HwiSubCommand,
752+
) -> Result<serde_json::Value, Error> {
753+
match subcommand {
754+
HwiSubCommand::Devices => {
755+
let devices = crate::utils::connect_to_hardware_wallet(
756+
wallet.network(),
757+
wallet_opts,
758+
Some(wallet),
759+
)
760+
.await?;
761+
let device = if let Some(device) = device {
762+
json!({
763+
"type": device.device_kind().to_string(),
764+
"fingerprint": device.get_master_fingerprint().await?.to_string(),
765+
"model": device.device_kind().to_string(),
766+
})
767+
} else {
768+
json!(null)
769+
};
770+
Ok(json!({ "devices": device }))
771+
}
772+
HwiSubCommand::Register => {
773+
let policy = wallet_opts.ext_descriptor.clone().ok_or_else(|| {
774+
Error::Generic("External descriptor required for wallet registration".to_string())
775+
})?;
776+
let wallet_name = wallet_opts.wallet.clone().ok_or_else(|| {
777+
Error::Generic("Wallet name is required for wallet registration".to_string())
778+
})?;
779+
780+
let home_dir = prepare_home_dir(None)?;
781+
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
782+
#[cfg(feature = "sqlite")]
783+
let wallet = {
784+
let mut persister = match &wallet_opts.database_type {
785+
DatabaseType::Sqlite => {
786+
let db_file = database_path.join("wallet.sqlite");
787+
let connection = Connection::open(db_file)?;
788+
log::debug!("Sqlite database opened successfully");
789+
connection
790+
}
791+
};
792+
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
793+
wallet.persist(&mut persister)?;
794+
wallet
795+
};
796+
#[cfg(not(feature = "sqlite"))]
797+
let wallet = new_wallet(network, wallet_opts)?;
798+
799+
let device = crate::utils::connect_to_hardware_wallet(
800+
wallet.network(),
801+
wallet_opts,
802+
Some(wallet),
803+
)
804+
.await?;
805+
let hmac = if let Some(device) = device {
806+
let hmac = device.register_wallet(&wallet_name, &policy).await?;
807+
hmac.map(|h| h.to_lower_hex_string())
808+
} else {
809+
None
810+
};
811+
Ok(json!({ "hmac": hmac }))
812+
}
813+
HwiSubCommand::Address => {
814+
let home_dir = prepare_home_dir(None)?;
815+
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
816+
#[cfg(feature = "sqlite")]
817+
let wallet = {
818+
let mut persister = match &wallet_opts.database_type {
819+
DatabaseType::Sqlite => {
820+
let db_file = database_path.join("wallet.sqlite");
821+
let connection = Connection::open(db_file)?;
822+
log::debug!("Sqlite database opened successfully");
823+
connection
824+
}
825+
};
826+
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
827+
wallet.persist(&mut persister)?;
828+
wallet
829+
};
830+
#[cfg(not(feature = "sqlite"))]
831+
let wallet = new_wallet(network, wallet_opts)?;
832+
833+
let address = wallet.next_unused_address(KeychainKind::External);
834+
Ok(json!({ "address": address.address }))
835+
}
836+
HwiSubCommand::Sign { psbt } => {
837+
let home_dir = prepare_home_dir(None)?;
838+
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
839+
#[cfg(feature = "sqlite")]
840+
let wallet = {
841+
let mut persister = match &wallet_opts.database_type {
842+
DatabaseType::Sqlite => {
843+
let db_file = database_path.join("wallet.sqlite");
844+
let connection = Connection::open(db_file)?;
845+
log::debug!("Sqlite database opened successfully");
846+
connection
847+
}
848+
};
849+
let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
850+
wallet.persist(&mut persister)?;
851+
wallet
852+
};
853+
#[cfg(not(feature = "sqlite"))]
854+
let wallet = new_wallet(network, wallet_opts)?;
855+
856+
let mut psbt = Psbt::from_str(&psbt)
857+
.map_err(|e| Error::Generic(format!("Failed to parse PSBT: {e}")))?;
858+
let device = crate::utils::connect_to_hardware_wallet(
859+
wallet.network(),
860+
wallet_opts,
861+
Some(wallet),
862+
)
863+
.await?;
864+
let signed_psbt = if let Some(device) = device {
865+
device
866+
.sign_tx(&mut psbt)
867+
.await
868+
.map_err(|e| Error::Generic(format!("Failed to sign PSBT: {e}")))?;
869+
Some(psbt.to_string())
870+
} else {
871+
None
872+
};
873+
Ok(json!({ "psbt": signed_psbt }))
874+
}
875+
}
876+
}
877+
796878
/// The global top level handler.
797879
pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
798880
let network = cli_opts.network;
@@ -837,7 +919,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
837919
wallet.persist(&mut persister)?;
838920
result
839921
};
840-
// #[cfg(not(any(feature = "sqlite")))]
922+
#[cfg(not(any(feature = "sqlite")))]
841923
let result = {
842924
let wallet = new_wallet(network, &wallet_opts)?;
843925
let blockchain_client =
@@ -870,8 +952,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
870952

871953
let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
872954
let result =
873-
handle_offline_wallet_subcommand(&mut wallet, &wallet_opts, offline_subcommand)
874-
.await?;
955+
handle_offline_wallet_subcommand(&mut wallet, &wallet_opts, offline_subcommand);
875956
wallet.persist(&mut persister)?;
876957
result
877958
};
@@ -996,7 +1077,6 @@ async fn respond(
9961077
subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
9971078
} => {
9981079
let value = handle_offline_wallet_subcommand(wallet, wallet_opts, offline_subcommand)
999-
.await
10001080
.map_err(|e| e.to_string())?;
10011081
Some(value)
10021082
}

0 commit comments

Comments
 (0)