From c1f20b2687005ff03a9ebaee1d6f5b50da151791 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 29 Aug 2024 08:59:40 +0400 Subject: [PATCH 1/8] Separate Python calls from the interface --- README.md | 5 +- src/implementations.rs | 1 + src/implementations/python_implementation.rs | 363 ++++++++++++++++ src/interface.rs | 421 +++++-------------- src/lib.rs | 55 ++- src/types.rs | 48 +++ 6 files changed, 552 insertions(+), 341 deletions(-) create mode 100644 src/implementations.rs create mode 100644 src/implementations/python_implementation.rs diff --git a/README.md b/README.md index 23c927f..9a492a4 100644 --- a/README.md +++ b/README.md @@ -58,15 +58,16 @@ use bitcoin::Network; use bitcoin::bip32::DerivationPath; use hwi::error::Error; use hwi::HWIClient; +use hwi::implementations::python_implementation::PythonHWIImplementation; use std::str::FromStr; fn main() -> Result<(), Error> { - let mut devices = HWIClient::enumerate()?; + let mut devices = HWIClient::::enumerate()?; if devices.is_empty() { panic!("No devices found!"); } let first_device = devices.remove(0)?; - let client = HWIClient::get_client(&first_device, true, Network::Bitcoin.into())?; + let client = HWIClient::::get_client(&first_device, true, Network::Bitcoin.into())?; let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); let s = client.sign_message("I love BDK wallet", &derivation_path)?; println!("{:?}", s.signature); diff --git a/src/implementations.rs b/src/implementations.rs new file mode 100644 index 0000000..082f357 --- /dev/null +++ b/src/implementations.rs @@ -0,0 +1 @@ +pub mod python_implementation; diff --git a/src/implementations/python_implementation.rs b/src/implementations/python_implementation.rs new file mode 100644 index 0000000..f1cb545 --- /dev/null +++ b/src/implementations/python_implementation.rs @@ -0,0 +1,363 @@ +use crate::error::Error; +use crate::types::{ + HWIAddressType, HWIChain, HWIDevice, HWIDeviceType, HWIImplementation, LogLevel, +}; +use bitcoin::Psbt; +use pyo3::{prelude::*, py_run}; +use std::ops::Deref; +use std::process::Command; + +/// Convenience class containing required Python objects +#[derive(Debug)] +struct HWILib { + commands: Py, + json_dumps: Py, +} + +impl HWILib { + pub fn initialize() -> Result { + Python::with_gil(|py| { + let commands: Py = PyModule::import_bound(py, "hwilib.commands")?.into(); + let json_dumps: Py = + PyModule::import_bound(py, "json")?.getattr("dumps")?.into(); + Ok(HWILib { + commands, + json_dumps, + }) + }) + } +} + +#[derive(Debug)] +pub struct PythonHWIImplementation { + hwilib: HWILib, + hw_client: PyObject, +} + +impl Deref for PythonHWIImplementation { + type Target = PyObject; + + fn deref(&self) -> &Self::Target { + &self.hw_client + } +} + +impl HWIImplementation for PythonHWIImplementation { + fn enumerate() -> Result { + let libs = HWILib::initialize()?; + Python::with_gil(|py| { + let output = libs.commands.getattr(py, "enumerate")?.call0(py)?; + let output = libs.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn get_client(device: &HWIDevice, expert: bool, chain: HWIChain) -> Result { + let libs = HWILib::initialize()?; + let hw_client = Python::with_gil(|py| { + let client_args = ( + device.device_type.to_string(), + &device.path, + "", + expert, + chain, + ); + libs.commands + .getattr(py, "get_client")? + .call1(py, client_args) + })?; + + Ok(Self { + hwilib: libs, + hw_client, + }) + } + + fn find_device( + password: Option<&str>, + device_type: Option, + fingerprint: Option<&str>, + expert: bool, + chain: HWIChain, + ) -> Result { + let libs = HWILib::initialize()?; + let hw_client = Python::with_gil(|py| { + let client_args = ( + password.unwrap_or(""), + device_type.map_or_else(String::new, |d| d.to_string()), + fingerprint.unwrap_or(""), + expert, + chain, + ); + let client = libs + .commands + .getattr(py, "find_device")? + .call1(py, client_args)?; + if client.is_none(py) { + return Err(Error::Hwi("device not found".to_string(), None)); + } + Ok(client) + })?; + + Ok(Self { + hwilib: libs, + hw_client, + }) + } + + fn get_master_xpub(&self, addrtype: HWIAddressType, account: u32) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client, addrtype, account); + let output = self + .hwilib + .commands + .getattr(py, "getmasterxpub")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn sign_tx(&self, psbt: &Psbt) -> Result { + Python::with_gil(|py| { + let output = self + .hwilib + .commands + .getattr(py, "signtx")? + .call1(py, (&self.hw_client, psbt.to_string()))?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn get_xpub(&self, path: &str, expert: bool) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client, path, expert); + let output = self + .hwilib + .commands + .getattr(py, "getxpub")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn sign_message(&self, message: &str, path: &str) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client, message, path); + let output = self + .hwilib + .commands + .getattr(py, "signmessage")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn get_keypool( + &self, + keypool: bool, + internal: bool, + addr_type: HWIAddressType, + addr_all: bool, + account: u32, + path: Option, + start: u32, + end: u32, + ) -> Result { + Python::with_gil(|py| { + let p_str = path.map_or(py.None(), |p| p.into_py(py)); + let func_args = ( + &self.hw_client, + p_str, + start, + end, + internal, + keypool, + account, + addr_type, + addr_all, + ); + let output = self + .hwilib + .commands + .getattr(py, "getkeypool")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn get_descriptors(&self, account: u32) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client, account); + let output = self + .hwilib + .commands + .getattr(py, "getdescriptors")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn display_address_with_desc(&self, descriptor: &str) -> Result { + Python::with_gil(|py| { + let path = py.None(); + let func_args = (&self.hw_client, path, descriptor); + let output = self + .hwilib + .commands + .getattr(py, "displayaddress")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn display_address_with_path( + &self, + path: &str, + address_type: HWIAddressType, + ) -> Result { + Python::with_gil(|py| { + let descriptor = py.None(); + let func_args = (&self.hw_client, path, descriptor, address_type); + let output = self + .hwilib + .commands + .getattr(py, "displayaddress")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn install_udev_rules(source: &str, location: &str) -> Result { + let libs = HWILib::initialize()?; + + Python::with_gil(|py| { + let func_args = (source, location); + let output = libs + .commands + .getattr(py, "install_udev_rules")? + .call1(py, func_args)?; + let output = libs.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn set_log_level(level: LogLevel) -> Result<(), Error> { + Python::with_gil(|py| { + let arg = match level { + LogLevel::DEBUG => 10, + LogLevel::INFO => 20, + LogLevel::WARNING => 30, + LogLevel::ERROR => 40, + LogLevel::CRITICAL => 50, + }; + py_run!( + py, + arg, + r#" + import logging + logging.basicConfig(level=arg) + "# + ); + Ok(()) + }) + } + + fn toggle_passphrase(&self) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client,); + let output = self + .hwilib + .commands + .getattr(py, "toggle_passphrase")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn setup_device(&self, label: &str, passphrase: &str) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client, label, passphrase); + let output = self + .hwilib + .commands + .getattr(py, "setup_device")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn restore_device(&self, label: &str, word_count: u8) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client, label, word_count); + let output = self + .hwilib + .commands + .getattr(py, "restore_device")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn backup_device(&self, label: &str, backup_passphrase: &str) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client, label, backup_passphrase); + let output = self + .hwilib + .commands + .getattr(py, "backup_device")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn wipe_device(&self) -> Result { + Python::with_gil(|py| { + let func_args = (&self.hw_client,); + let output = self + .hwilib + .commands + .getattr(py, "wipe_device")? + .call1(py, func_args)?; + let output = self.hwilib.json_dumps.call1(py, (output,))?; + Ok(output.to_string()) + }) + } + + fn get_version() -> Result { + Python::with_gil(|py| { + let hwilib = PyModule::import_bound(py, "hwilib")?; + let version = hwilib.getattr("__version__")?.extract::()?; + + Ok(version) + }) + } + + fn install_hwilib(version: String) -> Result<(), Error> { + let output = Command::new("pip") + .args(vec!["install", "--user", &version]) + .output() + .map_err(|e| Error::Hwi(format!("Failed to execute pip: {}", e), None))?; + + if output.status.success() { + Ok(()) + } else { + let error_message = String::from_utf8_lossy(&output.stderr).into_owned(); + Err(Error::Hwi( + format!("Failed to install HWI: {}", error_message), + None, + )) + } + } +} diff --git a/src/interface.rs b/src/interface.rs index e324bbe..f0504d0 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -1,6 +1,4 @@ use std::convert::TryInto; -use std::ops::Deref; -use std::process::Command; use bitcoin::bip32::DerivationPath; use bitcoin::Psbt; @@ -11,12 +9,10 @@ use serde_json::value::Value; use crate::error::Error; use crate::types::{ HWIAddress, HWIAddressType, HWIChain, HWIDescriptor, HWIDevice, HWIDeviceInternal, - HWIDeviceType, HWIExtendedPubKey, HWIKeyPoolElement, HWIPartiallySignedTransaction, - HWISignature, HWIStatus, HWIWordCount, LogLevel, ToDescriptor, + HWIDeviceType, HWIExtendedPubKey, HWIImplementation, HWIKeyPoolElement, + HWIPartiallySignedTransaction, HWISignature, HWIStatus, HWIWordCount, LogLevel, ToDescriptor, }; -use pyo3::{prelude::*, py_run}; - macro_rules! deserialize_obj { ( $e: expr ) => {{ let value: Value = serde_json::from_str($e)?; @@ -26,48 +22,18 @@ macro_rules! deserialize_obj { }}; } -/// Convenience class containing required Python objects -#[derive(Debug)] -struct HWILib { - commands: Py, - json_dumps: Py, -} - -impl HWILib { - pub fn initialize() -> Result { - Python::with_gil(|py| { - let commands: Py = PyModule::import_bound(py, "hwilib.commands")?.into(); - let json_dumps: Py = - PyModule::import_bound(py, "json")?.getattr("dumps")?.into(); - Ok(HWILib { - commands, - json_dumps, - }) - }) - } -} - -#[derive(Debug)] -pub struct HWIClient { - hwilib: HWILib, - hw_client: PyObject, +pub struct HWIClient { + implementation: T, } -impl Deref for HWIClient { - type Target = PyObject; - - fn deref(&self) -> &Self::Target { - &self.hw_client - } -} - -impl HWIClient { +impl HWIClient { /// Lists all HW devices currently connected. /// ```no_run /// # use hwi::HWIClient; + /// # use hwi::implementations::python_implementation::PythonHWIImplementation; /// # use hwi::error::Error; /// # fn main() -> Result<(), Error> { - /// let devices = HWIClient::enumerate()?; + /// let devices = HWIClient::::enumerate()?; /// for device in devices { /// match device { /// Ok(d) => println!("I can see a {} here 😄", d.model), @@ -78,13 +44,9 @@ impl HWIClient { /// # } /// ``` pub fn enumerate() -> Result>, Error> { - let libs = HWILib::initialize()?; - Python::with_gil(|py| { - let output = libs.commands.getattr(py, "enumerate")?.call0(py)?; - let output = libs.json_dumps.call1(py, (output,))?; - let devices_internal: Vec = deserialize_obj!(&output.to_string())?; - Ok(devices_internal.into_iter().map(|d| d.try_into()).collect()) - }) + let output = T::enumerate()?; + let devices_internal: Vec = deserialize_obj!(&output)?; + Ok(devices_internal.into_iter().map(|d| d.try_into()).collect()) } /// Returns the HWIClient for a certain device. You can list all the available devices using @@ -95,11 +57,16 @@ impl HWIClient { /// # use hwi::HWIClient; /// # use hwi::types::*; /// # use hwi::error::Error; + /// # use hwi::implementations::python_implementation::PythonHWIImplementation; /// # fn main() -> Result<(), Error> { - /// let devices = HWIClient::enumerate()?; + /// let devices = HWIClient::::enumerate()?; /// for device in devices { /// let device = device?; - /// let client = HWIClient::get_client(&device, false, bitcoin::Network::Testnet.into())?; + /// let client = HWIClient::::get_client( + /// &device, + /// false, + /// bitcoin::Network::Testnet.into(), + /// )?; /// let xpub = client.get_master_xpub(HWIAddressType::Tap, 0)?; /// println!( /// "I can see a {} here, and its xpub is {}", @@ -110,29 +77,10 @@ impl HWIClient { /// # Ok(()) /// # } /// ``` - pub fn get_client( - device: &HWIDevice, - expert: bool, - chain: HWIChain, - ) -> Result { - let libs = HWILib::initialize()?; - Python::with_gil(|py| { - let client_args = ( - device.device_type.to_string(), - &device.path, - "", - expert, - chain, - ); - let client = libs - .commands - .getattr(py, "get_client")? - .call1(py, client_args)?; - Ok(HWIClient { - hwilib: libs, - hw_client: client, - }) - }) + pub fn get_client(device: &HWIDevice, expert: bool, chain: HWIChain) -> Result { + let implementation = T::get_client(device, expert, chain)?; + + Ok(Self { implementation }) } /// Returns the HWIClient for a certain `device_type` or `fingerprint`. You can list all the available devices using @@ -143,8 +91,9 @@ impl HWIClient { /// # use hwi::HWIClient; /// # use hwi::types::*; /// # use hwi::error::Error; + /// # use hwi::implementations::python_implementation::PythonHWIImplementation; /// # fn main() -> Result<(), Error> { - /// let client = HWIClient::find_device( + /// let client = HWIClient::::find_device( /// None, /// Some(HWIDeviceType::Trezor), /// None, @@ -162,30 +111,16 @@ impl HWIClient { fingerprint: Option<&str>, expert: bool, chain: bitcoin::Network, - ) -> Result { - let libs = HWILib::initialize()?; - Python::with_gil(|py| { - let client_args = ( - password.unwrap_or(""), - device_type.map_or_else(String::new, |d| d.to_string()), - fingerprint.unwrap_or(""), - expert, - HWIChain::from(chain), - ); - let client = libs - .commands - .getattr(py, "find_device")? - .call1(py, client_args)?; - - if client.is_none(py) { - return Err(Error::Hwi("device not found".to_string(), None)); - } - - Ok(HWIClient { - hwilib: libs, - hw_client: client, - }) - }) + ) -> Result { + let implementation = T::find_device( + password, + device_type, + fingerprint, + expert, + HWIChain::from(chain), + )?; + + Ok(Self { implementation }) } /// Returns the master xpub of a device, given the address type and the account number. @@ -194,28 +129,14 @@ impl HWIClient { addrtype: HWIAddressType, account: u32, ) -> Result { - Python::with_gil(|py| { - let output = self - .hwilib - .commands - .getattr(py, "getmasterxpub")? - .call1(py, (&self.hw_client, addrtype, account))?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - deserialize_obj!(&output.to_string()) - }) + let output = self.implementation.get_master_xpub(addrtype, account)?; + deserialize_obj!(&output) } /// Signs a PSBT. pub fn sign_tx(&self, psbt: &Psbt) -> Result { - Python::with_gil(|py| { - let output = self - .hwilib - .commands - .getattr(py, "signtx")? - .call1(py, (&self.hw_client, psbt.to_string()))?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - deserialize_obj!(&output.to_string()) - }) + let output = self.implementation.sign_tx(psbt)?; + deserialize_obj!(&output) } /// Returns the xpub of a device. If `expert` is set, additional output is returned. @@ -225,16 +146,8 @@ impl HWIClient { expert: bool, ) -> Result { let prefixed_path = format!("m/{}", path); - Python::with_gil(|py| { - let func_args = (&self.hw_client, prefixed_path, expert); - let output = self - .hwilib - .commands - .getattr(py, "getxpub")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - deserialize_obj!(&output.to_string()) - }) + let output = self.implementation.get_xpub(&prefixed_path, expert)?; + deserialize_obj!(&output) } /// Signs a message. @@ -244,16 +157,8 @@ impl HWIClient { path: &DerivationPath, ) -> Result { let prefixed_path = format!("m/{}", path); - Python::with_gil(|py| { - let func_args = (&self.hw_client, message, prefixed_path); - let output = self - .hwilib - .commands - .getattr(py, "signmessage")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - deserialize_obj!(&output.to_string()) - }) + let output = self.implementation.sign_message(message, &prefixed_path)?; + deserialize_obj!(&output) } /// Returns an array of keys that can be imported in Bitcoin core using importmulti @@ -278,66 +183,37 @@ impl HWIClient { start: u32, end: u32, ) -> Result, Error> { - Python::with_gil(|py| { - let mut p_str = py.None(); - if let Some(p) = path { - p_str = format!("m/{}/*", p).into_py(py); - } - let func_args = ( - &self.hw_client, - p_str, - start, - end, - internal, - keypool, - account.unwrap_or(0), - addr_type, - addr_all, - ); - let output = self - .hwilib - .commands - .getattr(py, "getkeypool")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - deserialize_obj!(&output.to_string()) - }) + let path_str = path.map(|p| format!("m/{}/*", p)); + let output = self.implementation.get_keypool( + keypool, + internal, + addr_type, + addr_all, + account.unwrap_or(0), + path_str, + start, + end, + )?; + deserialize_obj!(&output) } /// Returns device descriptors. You can optionally specify a BIP43 account to use. - pub fn get_descriptors(&self, account: Option) -> Result, Error> + pub fn get_descriptors(&self, account: Option) -> Result, Error> where - T: ToDescriptor + DeserializeOwned, + U: ToDescriptor + DeserializeOwned, { - Python::with_gil(|py| { - let func_args = (&self.hw_client, account.unwrap_or(0)); - let output = self - .hwilib - .commands - .getattr(py, "getdescriptors")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - deserialize_obj!(&output.to_string()) - }) + let output = self.implementation.get_descriptors(account.unwrap_or(0))?; + deserialize_obj!(&output) } /// Returns an address given a descriptor. - pub fn display_address_with_desc(&self, descriptor: &T) -> Result + pub fn display_address_with_desc(&self, descriptor: &U) -> Result where - T: ToDescriptor + ToString, + U: ToDescriptor + ToString, { - Python::with_gil(|py| { - let path = py.None(); - let descriptor = descriptor.to_string().split('#').collect::>()[0].to_string(); - let func_args = (&self.hw_client, path, descriptor); - let output = self - .hwilib - .commands - .getattr(py, "displayaddress")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - deserialize_obj!(&output.to_string()) - }) + let descriptor = descriptor.to_string().split('#').collect::>()[0].to_string(); + let output = self.implementation.display_address_with_desc(&descriptor)?; + deserialize_obj!(&output) } /// Returns an address given path and address type. @@ -346,18 +222,11 @@ impl HWIClient { path: &DerivationPath, address_type: HWIAddressType, ) -> Result { - Python::with_gil(|py| { - let prefixed_path = format!("m/{}", path); - let descriptor = py.None(); - let func_args = (&self.hw_client, prefixed_path, descriptor, address_type); - let output = self - .hwilib - .commands - .getattr(py, "displayaddress")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - deserialize_obj!(&output.to_string()) - }) + let prefixed_path = format!("m/{}", path); + let output = self + .implementation + .display_address_with_path(&prefixed_path, address_type)?; + deserialize_obj!(&output) } /// Install the udev rules to the local machine. @@ -365,78 +234,36 @@ impl HWIClient { /// The rules will be copied from the source to the location; the default source location is /// `./udev`, the default destination location is `/lib/udev/rules.d` pub fn install_udev_rules(source: Option<&str>, location: Option<&str>) -> Result<(), Error> { - Python::with_gil(|py| { - let libs = HWILib::initialize()?; - let func_args = ( - source.unwrap_or("./udev"), - location.unwrap_or("/lib/udev/rules.d/"), - ); - let output = libs - .commands - .getattr(py, "install_udev_rules")? - .call1(py, func_args)?; - let output = libs.json_dumps.call1(py, (output,))?; - let status: HWIStatus = deserialize_obj!(&output.to_string())?; - status.into() - }) + let output = T::install_udev_rules( + source.unwrap_or("./udev"), + location.unwrap_or("/lib/udev/rules.d/"), + )?; + let status: HWIStatus = deserialize_obj!(&output)?; + status.into() } /// Set logging level /// # Arguments /// * `level` - Log level. pub fn set_log_level(level: LogLevel) -> Result<(), Error> { - Python::with_gil(|py| { - let arg = match level { - LogLevel::DEBUG => 10, - LogLevel::INFO => 20, - LogLevel::WARNING => 30, - LogLevel::ERROR => 40, - LogLevel::CRITICAL => 50, - }; - py_run!( - py, - arg, - r#" - import logging - logging.basicConfig(level=arg) - "# - ); - Ok(()) - }) + T::set_log_level(level)?; + Ok(()) } /// Toggle whether the device is using a BIP 39 passphrase. pub fn toggle_passphrase(&self) -> Result<(), Error> { - Python::with_gil(|py| { - let func_args = (&self.hw_client,); - let output = self - .hwilib - .commands - .getattr(py, "toggle_passphrase")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - let status: HWIStatus = deserialize_obj!(&output.to_string())?; - status.into() - }) + let output = self.implementation.toggle_passphrase()?; + let status: HWIStatus = deserialize_obj!(&output)?; + status.into() } - /// Setup a device + /// Set up the device pub fn setup_device(&self, label: Option<&str>, passphrase: Option<&str>) -> Result<(), Error> { - Python::with_gil(|py| { - let func_args = ( - &self.hw_client, - label.unwrap_or(""), - passphrase.unwrap_or(""), - ); - let output = self - .hwilib - .commands - .getattr(py, "setup_device")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - let status: HWIStatus = deserialize_obj!(&output.to_string())?; - status.into() - }) + let output = self + .implementation + .setup_device(label.unwrap_or(""), passphrase.unwrap_or(""))?; + let status: HWIStatus = deserialize_obj!(&output)?; + status.into() } /// Restore a device @@ -445,18 +272,12 @@ impl HWIClient { label: Option<&str>, word_count: Option, ) -> Result<(), Error> { - Python::with_gil(|py| { - let word_count: u8 = word_count.map_or_else(|| 24, |w| w as u8); - let func_args = (&self.hw_client, label.unwrap_or(""), word_count); - let output = self - .hwilib - .commands - .getattr(py, "restore_device")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - let status: HWIStatus = deserialize_obj!(&output.to_string())?; - status.into() - }) + let word_count: u8 = word_count.map_or_else(|| 24, |w| w as u8); + let output = self + .implementation + .restore_device(label.unwrap_or(""), word_count)?; + let status: HWIStatus = deserialize_obj!(&output)?; + status.into() } /// Create a backup of the device @@ -465,49 +286,24 @@ impl HWIClient { label: Option<&str>, backup_passphrase: Option<&str>, ) -> Result<(), Error> { - Python::with_gil(|py| { - let func_args = ( - &self.hw_client, - label.unwrap_or_default(), - backup_passphrase.unwrap_or_default(), - ); - let output = self - .hwilib - .commands - .getattr(py, "backup_device")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - let status: HWIStatus = deserialize_obj!(&output.to_string())?; - status.into() - }) + let output = self.implementation.backup_device( + label.unwrap_or_default(), + backup_passphrase.unwrap_or_default(), + )?; + let status: HWIStatus = deserialize_obj!(&output)?; + status.into() } /// Wipe a device pub fn wipe_device(&self) -> Result<(), Error> { - Python::with_gil(|py| { - let func_args = (&self.hw_client,); - let output = self - .hwilib - .commands - .getattr(py, "wipe_device")? - .call1(py, func_args)?; - let output = self.hwilib.json_dumps.call1(py, (output,))?; - let status: HWIStatus = deserialize_obj!(&output.to_string())?; - status.into() - }) + let output = self.implementation.wipe_device()?; + let status: HWIStatus = deserialize_obj!(&output)?; + status.into() } /// Get the installed version of hwilib. Returns None if hwi is not installed. - pub fn get_version() -> Option { - Python::with_gil(|py| { - Some( - PyModule::import_bound(py, "hwilib") - .ok()? - .getattr("__version__") - .expect("Should have a __version__") - .to_string(), - ) - }) + pub fn get_version() -> Result { + T::get_version() } /// Install hwi for the current user via pip. If no version is specified, the default version from pip will be installed. @@ -516,18 +312,7 @@ impl HWIClient { Some(ver) => "hwi==".to_owned() + ver, None => "hwi".to_owned(), }; - let output = Command::new("pip") - .args(vec!["install", "--user", hwi_with_version.as_str()]) - .output()?; - if output.status.success() { - Ok(()) - } else { - Err(Error::Hwi( - std::str::from_utf8(&output.stderr) - .expect("Non UTF-8 error while installing") - .to_string(), - None, - )) - } + T::install_hwilib(hwi_with_version)?; + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index d549e92..ef12645 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,19 +4,24 @@ //! ```no_run //! use bitcoin::bip32::{ChildNumber, DerivationPath}; //! use hwi::error::Error; +//! use hwi::implementations::python_implementation::PythonHWIImplementation; //! use hwi::interface::HWIClient; //! use hwi::types; //! use std::str::FromStr; //! //! fn main() -> Result<(), Error> { //! // Find information about devices -//! let mut devices = HWIClient::enumerate()?; +//! let mut devices = HWIClient::::enumerate()?; //! if devices.is_empty() { //! panic!("No device found!"); //! } //! let device = devices.remove(0)?; //! // Create a client for a device -//! let client = HWIClient::get_client(&device, true, bitcoin::Network::Testnet.into())?; +//! let client = HWIClient::::get_client( +//! &device, +//! true, +//! bitcoin::Network::Testnet.into(), +//! )?; //! // Display the address from path //! let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); //! let hwi_address = @@ -36,11 +41,13 @@ pub use interface::HWIClient; #[cfg(feature = "doctest")] pub mod doctest; pub mod error; +pub mod implementations; pub mod interface; pub mod types; #[cfg(test)] mod tests { + use crate::implementations::python_implementation::PythonHWIImplementation; use crate::types::{self, HWIDeviceType, TESTNET}; use crate::HWIClient; use std::collections::BTreeMap; @@ -58,7 +65,7 @@ mod tests { #[test] #[serial] fn test_enumerate() { - let devices = HWIClient::enumerate().unwrap(); + let devices = HWIClient::::enumerate().unwrap(); assert!(!devices.is_empty()); } @@ -66,7 +73,7 @@ mod tests { #[serial] #[ignore] fn test_find_trezor_device() { - HWIClient::find_device( + HWIClient::::find_device( None, Some(HWIDeviceType::Trezor), None, @@ -76,14 +83,14 @@ mod tests { .unwrap(); } - fn get_first_device() -> HWIClient { - let devices = HWIClient::enumerate().unwrap(); + fn get_first_device() -> HWIClient { + let devices = HWIClient::::enumerate().unwrap(); let device = devices .first() .expect("No devices found. Either plug in a hardware wallet, or start a simulator.") .as_ref() .expect("Error when opening the first device"); - HWIClient::get_client(device, true, TESTNET).unwrap() + HWIClient::::get_client(device, true, TESTNET).unwrap() } #[test] @@ -194,12 +201,14 @@ mod tests { // #[serial] // fn test_display_address_with_path_taproot() {} + // For testing on Coldcard, make sure to disable Max Network Fee in the Coldcard settings #[test] #[serial] fn test_sign_tx() { - let devices = HWIClient::enumerate().unwrap(); + let devices = HWIClient::::enumerate().unwrap(); let device = devices.first().unwrap().as_ref().unwrap(); - let client = HWIClient::get_client(device, true, TESTNET).unwrap(); + let client = + HWIClient::::get_client(device, true, TESTNET).unwrap(); let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); let address = client @@ -325,21 +334,21 @@ mod tests { #[ignore] fn test_install_udev_rules() { if cfg!(target_os = "linux") { - HWIClient::install_udev_rules(None, None).unwrap() + HWIClient::::install_udev_rules(None, None).unwrap() } } #[test] #[serial] fn test_set_log_level() { - HWIClient::set_log_level(types::LogLevel::DEBUG).unwrap(); + HWIClient::::set_log_level(types::LogLevel::DEBUG).unwrap(); test_enumerate(); } #[test] #[serial] fn test_toggle_passphrase() { - let devices = HWIClient::enumerate().unwrap(); + let devices = HWIClient::::enumerate().unwrap(); let unsupported = [ HWIDeviceType::Ledger, HWIDeviceType::BitBox01, @@ -352,7 +361,8 @@ mod tests { // These devices don't support togglepassphrase continue; } - let client = HWIClient::get_client(&device, true, TESTNET).unwrap(); + let client = + HWIClient::::get_client(&device, true, TESTNET).unwrap(); client.toggle_passphrase().unwrap(); break; } @@ -361,7 +371,7 @@ mod tests { #[test] #[serial] fn test_get_version() { - HWIClient::get_version().unwrap(); + HWIClient::::get_version().unwrap(); } #[test] @@ -369,7 +379,7 @@ mod tests { #[ignore] // At the moment (hwi v2.1.1 and trezor-firmware core v2.5.2) work only with physical devices and NOT emulators! fn test_setup_trezor_device() { - let client = HWIClient::find_device( + let client = HWIClient::::find_device( None, Some(HWIDeviceType::Trezor), None, @@ -385,7 +395,7 @@ mod tests { #[ignore] // At the moment (hwi v2.1.1 and trezor-firmware core v2.5.2) work only with physical devices and NOT emulators! fn test_restore_trezor_device() { - let client = HWIClient::find_device( + let client = HWIClient::::find_device( None, Some(HWIDeviceType::Trezor), None, @@ -399,7 +409,7 @@ mod tests { #[test] #[serial] fn test_backup_device() { - let devices = HWIClient::enumerate().unwrap(); + let devices = HWIClient::::enumerate().unwrap(); let supported = [ HWIDeviceType::BitBox01, HWIDeviceType::BitBox02, @@ -408,7 +418,9 @@ mod tests { for device in devices { let device = device.unwrap(); if supported.contains(&device.device_type) { - let client = HWIClient::get_client(&device, true, TESTNET).unwrap(); + let client = + HWIClient::::get_client(&device, true, TESTNET) + .unwrap(); client.backup_device(Some("My Label"), None).unwrap(); } } @@ -418,7 +430,7 @@ mod tests { #[serial] #[ignore] fn test_wipe_device() { - let devices = HWIClient::enumerate().unwrap(); + let devices = HWIClient::::enumerate().unwrap(); let unsupported = [ HWIDeviceType::Ledger, HWIDeviceType::Coldcard, @@ -430,7 +442,8 @@ mod tests { // These devices don't support wipe continue; } - let client = HWIClient::get_client(&device, true, TESTNET).unwrap(); + let client = + HWIClient::::get_client(&device, true, TESTNET).unwrap(); client.wipe_device().unwrap(); } } @@ -439,6 +452,6 @@ mod tests { #[serial] #[ignore] fn test_install_hwi() { - HWIClient::install_hwilib(Some("2.1.1")).unwrap(); + HWIClient::::install_hwilib(Some("2.1.1")).unwrap(); } } diff --git a/src/types.rs b/src/types.rs index db25a24..6678541 100644 --- a/src/types.rs +++ b/src/types.rs @@ -295,3 +295,51 @@ pub enum HWIWordCount { W18 = 18, W24 = 24, } + +pub trait HWIImplementation { + fn enumerate() -> Result; + fn get_client(device: &HWIDevice, expert: bool, chain: HWIChain) -> Result + where + Self: Sized; + fn find_device( + password: Option<&str>, + device_type: Option, + fingerprint: Option<&str>, + expert: bool, + chain: HWIChain, + ) -> Result + where + Self: Sized; + fn get_xpub(&self, path: &str, expert: bool) -> Result; + fn sign_tx(&self, psbt: &Psbt) -> Result; + fn get_master_xpub(&self, addrtype: HWIAddressType, account: u32) -> Result; + fn sign_message(&self, message: &str, path: &str) -> Result; + fn display_address_with_desc(&self, descriptor: &str) -> Result; + fn display_address_with_path( + &self, + path: &str, + address_type: HWIAddressType, + ) -> Result; + fn toggle_passphrase(&self) -> Result; + fn setup_device(&self, label: &str, passphrase: &str) -> Result; + fn restore_device(&self, label: &str, word_count: u8) -> Result; + fn backup_device(&self, label: &str, backup_passphrase: &str) -> Result; + fn wipe_device(&self) -> Result; + fn get_descriptors(&self, account: u32) -> Result; + #[allow(clippy::too_many_arguments)] + fn get_keypool( + &self, + keypool: bool, + internal: bool, + addr_type: HWIAddressType, + addr_all: bool, + account: u32, + path: Option, + start: u32, + end: u32, + ) -> Result; + fn get_version() -> Result; + fn install_udev_rules(source: &str, location: &str) -> Result; + fn set_log_level(level: LogLevel) -> Result<(), Error>; + fn install_hwilib(version: String) -> Result<(), Error>; +} From 78ddcf920e6489e1ee7b43d0d51b9dbc1f29771a Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 29 Aug 2024 09:00:53 +0400 Subject: [PATCH 2/8] Add HWI binary implementation --- src/error.rs | 3 + src/implementations.rs | 1 + src/implementations/binary_implementation.rs | 326 +++++++++++++++++++ src/types.rs | 28 ++ 4 files changed, 358 insertions(+) create mode 100644 src/implementations/binary_implementation.rs diff --git a/src/error.rs b/src/error.rs index aa983fb..1619507 100644 --- a/src/error.rs +++ b/src/error.rs @@ -78,6 +78,7 @@ pub enum Error { Io(std::io::Error), Hwi(String, Option), Python(pyo3::PyErr), + NotImplemented, } impl fmt::Display for Error { @@ -90,6 +91,7 @@ impl fmt::Display for Error { Io(_) => f.write_str("I/O error"), Hwi(ref s, ref code) => write!(f, "HWI error: {}, ({:?})", s, code), Python(_) => f.write_str("python error"), + NotImplemented => f.write_str("not implemented"), } } } @@ -104,6 +106,7 @@ impl std::error::Error for Error { Io(ref e) => Some(e), Hwi(_, _) => None, Python(ref e) => Some(e), + NotImplemented => None, } } } diff --git a/src/implementations.rs b/src/implementations.rs index 082f357..4a513c5 100644 --- a/src/implementations.rs +++ b/src/implementations.rs @@ -1 +1,2 @@ +pub mod binary_implementation; pub mod python_implementation; diff --git a/src/implementations/binary_implementation.rs b/src/implementations/binary_implementation.rs new file mode 100644 index 0000000..a10356d --- /dev/null +++ b/src/implementations/binary_implementation.rs @@ -0,0 +1,326 @@ +use serde_json::value::Value; +use std::str; + +use crate::error::Error; +use crate::types::{ + HWIAddressType, HWIBinaryExecutor, HWIChain, HWIDevice, HWIDeviceType, HWIImplementation, + LogLevel, +}; +use bitcoin::Psbt; + +macro_rules! deserialize_obj { + ( $e: expr ) => {{ + let value: Value = serde_json::from_str($e)?; + let obj = value.clone(); + serde_json::from_value(value) + .map_err(|e| Error::Hwi(format!("error {} while deserializing {}", e, obj), None)) + }}; +} + +#[derive(Debug)] +pub struct BinaryHWIImplementation { + device: Option, + expert: bool, + chain: HWIChain, + _phantom: std::marker::PhantomData, +} + +impl HWIImplementation for BinaryHWIImplementation { + fn enumerate() -> Result { + let output = + BinaryHWIImplementation::::run_hwi_command(None, false, None, vec!["enumerate"])?; + Ok(output.to_string()) + } + + fn get_client(device: &HWIDevice, expert: bool, chain: HWIChain) -> Result { + Ok(Self { + device: Some(device.clone()), + expert, + chain, + _phantom: std::marker::PhantomData, + }) + } + + fn find_device( + password: Option<&str>, + device_type: Option, + fingerprint: Option<&str>, + expert: bool, + chain: HWIChain, + ) -> Result { + let mut client = BinaryHWIImplementation { + device: None, + expert, + chain, + _phantom: std::marker::PhantomData, + }; + + let mut args = vec!["enumerate"]; + + if let Some(pw) = password { + args.extend_from_slice(&["--password", pw]); + } + + let output = + BinaryHWIImplementation::::run_hwi_command(None, expert, Some(&client.chain), args)?; + let devices: Vec = deserialize_obj!(&output)?; + + let device = devices + .into_iter() + .find(|d| { + device_type.as_ref().map_or(true, |t| &d.device_type == t) + && fingerprint.map_or(true, |f| d.fingerprint.to_string() == f) + }) + .ok_or_else(|| Error::Hwi("No matching device found".to_string(), None))?; + + client.device = Some(device); + Ok(client) + } + + fn get_master_xpub(&self, addrtype: HWIAddressType, account: u32) -> Result { + let mut args = vec!["getmasterxpub"]; + let addrtype_str = addrtype.to_string(); + let account_str = account.to_string(); + args.extend_from_slice(&["--addr-type", &addrtype_str]); + args.extend_from_slice(&["--account", &account_str]); + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn sign_tx(&self, psbt: &Psbt) -> Result { + let psbt_str = psbt.to_string(); + let args = vec!["signtx", &psbt_str]; + + let output = BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + )?; + Ok(output) + } + + fn get_xpub(&self, path: &str, expert: bool) -> Result { + let args = vec!["getxpub", &path]; + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + expert, + Some(&self.chain), + args, + ) + } + + fn sign_message(&self, message: &str, path: &str) -> Result { + let args = vec!["signmessage", message, path]; + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn get_keypool( + &self, + keypool: bool, + internal: bool, + addr_type: HWIAddressType, + addr_all: bool, + account: u32, + path: Option, + start: u32, + end: u32, + ) -> Result { + let mut args = vec!["getkeypool"]; + + if keypool { + args.push("--keypool"); + } + if internal { + args.push("--internal"); + } + let addrtype_str = addr_type.to_string(); + args.extend_from_slice(&["--addr-type", &addrtype_str]); + if addr_all { + args.push("--addr-all"); + } + let account_str = account.to_string(); + args.extend_from_slice(&["--account", &account_str]); + if let Some(p) = path.as_deref() { + args.extend_from_slice(&["--path", p]); + } + let start_str = start.to_string(); + args.push(&start_str); + let end_str = end.to_string(); + args.push(&end_str); + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn get_descriptors(&self, account: u32) -> Result { + let mut args = vec!["getdescriptors"]; + let account_str = account.to_string(); + args.extend_from_slice(&["--account", &account_str]); + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn display_address_with_desc(&self, descriptor: &str) -> Result { + let mut args = vec!["displayaddress"]; + args.push("--desc"); + args.push(descriptor); + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn display_address_with_path( + &self, + path: &str, + address_type: HWIAddressType, + ) -> Result { + let mut args = vec!["displayaddress"]; + args.extend_from_slice(&["--path", path]); + let addr_type_str = address_type.to_string(); + args.extend_from_slice(&["--addr-type", &addr_type_str]); + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn install_udev_rules(_: &str, location: &str) -> Result { + let mut args = vec!["installudevrules"]; + args.extend_from_slice(&["--location", location]); + + BinaryHWIImplementation::::run_hwi_command(None, false, None, args) + } + + fn set_log_level(_: LogLevel) -> Result<(), Error> { + Err(Error::NotImplemented) + } + + fn toggle_passphrase(&self) -> Result { + let args = vec!["togglepassphrase"]; + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn setup_device(&self, label: &str, passphrase: &str) -> Result { + let mut args = vec!["setup"]; + args.extend_from_slice(&["--label", label]); + args.extend_from_slice(&["--backup_passphrase", passphrase]); + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn restore_device(&self, label: &str, word_count: u8) -> Result { + let mut args = vec!["restore"]; + let word_count_str = word_count.to_string(); + args.extend_from_slice(&["--word_count", &word_count_str]); + args.extend_from_slice(&["--label", label]); + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + fn backup_device(&self, label: &str, backup_passphrase: &str) -> Result { + let mut args = vec!["backup"]; + args.extend_from_slice(&["--label", label]); + args.extend_from_slice(&["--backup_passphrase", backup_passphrase]); + + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn wipe_device(&self) -> Result { + let args = vec!["wipe"]; + BinaryHWIImplementation::::run_hwi_command( + self.device.as_ref(), + self.expert, + Some(&self.chain), + args, + ) + } + + fn get_version() -> Result { + let args = vec!["--version"]; + BinaryHWIImplementation::::run_hwi_command(None, false, None, args) + } + + fn install_hwilib(_: String) -> Result<(), Error> { + Err(Error::NotImplemented) + } +} + +impl BinaryHWIImplementation { + fn run_hwi_command( + device: Option<&HWIDevice>, + expert: bool, + chain: Option<&HWIChain>, + args: Vec<&str>, + ) -> Result { + let mut command_args = Vec::new(); + + if !args.contains(&"enumerate") && !args.contains(&"--version") { + let fingerprint = device + .ok_or(Error::Hwi("Device fingerprint not set".to_string(), None))? + .fingerprint; + command_args.push("--fingerprint".to_string()); + command_args.push(fingerprint.to_string()); + } + + if expert { + command_args.push("--expert".to_string()); + } + + if let Some(c) = chain { + command_args.push("--chain".to_string()); + command_args.push(c.to_string()); + } + + command_args.extend(args.iter().map(|s| s.to_string())); + + T::execute_command(command_args) + } +} diff --git a/src/types.rs b/src/types.rs index 6678541..c4bface 100644 --- a/src/types.rs +++ b/src/types.rs @@ -112,6 +112,17 @@ pub enum HWIAddressType { Tap, } +impl fmt::Display for HWIAddressType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + HWIAddressType::Legacy => "LEGACY", + HWIAddressType::Sh_Wit => "SH_WIT", + HWIAddressType::Wit => "WIT", + HWIAddressType::Tap => "TAP", + }) + } +} + impl IntoPy for HWIAddressType { fn into_py(self, py: pyo3::Python) -> PyObject { let addrtype = PyModule::import_bound(py, "hwilib.common") @@ -130,6 +141,18 @@ impl IntoPy for HWIAddressType { #[derive(Clone, Eq, PartialEq, Debug, Deserialize)] pub struct HWIChain(bitcoin::Network); +impl fmt::Display for HWIChain { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self.0 { + bitcoin::Network::Bitcoin => "MAIN", + bitcoin::Network::Testnet => "TEST", + bitcoin::Network::Regtest => "REGTEST", + bitcoin::Network::Signet => "SIGNET", + _ => "UNKNOWN", + }) + } +} + impl IntoPy for HWIChain { fn into_py(self, py: pyo3::Python) -> PyObject { use bitcoin::Network::*; @@ -234,6 +257,7 @@ impl From for Result<(), Error> { } #[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum HWIDeviceType { Ledger, Trezor, @@ -343,3 +367,7 @@ pub trait HWIImplementation { fn set_log_level(level: LogLevel) -> Result<(), Error>; fn install_hwilib(version: String) -> Result<(), Error>; } + +pub trait HWIBinaryExecutor { + fn execute_command(args: Vec) -> Result; +} From 74674968d744a7e4e9175de9695e73a3a39d4df7 Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 30 Aug 2024 14:06:42 +0400 Subject: [PATCH 3/8] Run tests on multiple implementations --- .github/workflows/main.yml | 2 + .gitignore | 3 + get_hwi.sh | 58 +++ src/lib.rs | 745 +++++++++++++++++++------------------ 4 files changed, 456 insertions(+), 352 deletions(-) create mode 100755 get_hwi.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9a0f4d7..4d9ff3a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,6 +76,8 @@ jobs: run: rustup set profile minimal - name: Update toolchain run: rustup update + - name: Download and Install HWI + run: ./get_hwi.sh - name: Test run: cargo test --features ${{ matrix.rust.features }} - name: Wipe diff --git a/.gitignore b/.gitignore index bf450b5..2114d2d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ tags venv/ backup* + +# Binary downloaded for tests +hwi-binary/ \ No newline at end of file diff --git a/get_hwi.sh b/get_hwi.sh new file mode 100755 index 0000000..15d4740 --- /dev/null +++ b/get_hwi.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set -e + +# Function to detect OS and architecture +detect_platform() { + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + ARCH=$(uname -m) + + case $OS in + linux) + case $ARCH in + x86_64) PLATFORM="linux-x86_64" ;; + aarch64) PLATFORM="linux-aarch64" ;; + *) echo "Unsupported Linux architecture: $ARCH"; exit 1 ;; + esac + ;; + darwin) + case $ARCH in + x86_64) PLATFORM="mac-x86_64" ;; + arm64) PLATFORM="mac-arm64" ;; + *) echo "Unsupported macOS architecture: $ARCH"; exit 1 ;; + esac + ;; + *) echo "Unsupported OS: $OS"; exit 1 ;; + esac + + echo $PLATFORM +} + +# Detect platform +PLATFORM=$(detect_platform) + +# Set HWI version +HWI_VERSION="2.3.1" + +# Set download URL +DOWNLOAD_URL="https://github.com/bitcoin-core/HWI/releases/download/${HWI_VERSION}/hwi-${HWI_VERSION}-${PLATFORM}.tar.gz" + +# Set output directory +OUTPUT_DIR="hwi-binary" + +# Create output directory if it doesn't exist +mkdir -p $OUTPUT_DIR +chmod 755 $OUTPUT_DIR + +# Download and extract HWI +echo "Downloading HWI for $PLATFORM..." +curl -L $DOWNLOAD_URL -o hwi.tar.gz +echo "Download completed. Extracting..." +chmod 644 hwi.tar.gz +tar xzvf hwi.tar.gz -C $OUTPUT_DIR +rm hwi.tar.gz + +# Make the binary executable +chmod +x $OUTPUT_DIR/hwi + +echo "HWI binary downloaded and extracted to $OUTPUT_DIR/hwi" diff --git a/src/lib.rs b/src/lib.rs index ef12645..c9b2fb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,8 +47,10 @@ pub mod types; #[cfg(test)] mod tests { + use crate::error::Error; + use crate::implementations::binary_implementation::BinaryHWIImplementation; use crate::implementations::python_implementation::PythonHWIImplementation; - use crate::types::{self, HWIDeviceType, TESTNET}; + use crate::types::{self, HWIBinaryExecutor, HWIDeviceType, TESTNET}; use crate::HWIClient; use std::collections::BTreeMap; use std::str::FromStr; @@ -58,394 +60,433 @@ mod tests { use bitcoin::psbt::{Input, Output}; use bitcoin::{secp256k1, Transaction}; use bitcoin::{transaction, Amount, Network, TxIn, TxOut}; + use std::path::PathBuf; + use std::str; #[cfg(feature = "miniscript")] use miniscript::{Descriptor, DescriptorPublicKey}; - - #[test] - #[serial] - fn test_enumerate() { - let devices = HWIClient::::enumerate().unwrap(); - assert!(!devices.is_empty()); + struct HWIBinaryExecutorImpl; + + impl HWIBinaryExecutor for HWIBinaryExecutorImpl { + fn execute_command(args: Vec) -> Result { + let mut binary_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + binary_path.push("hwi-binary/hwi"); + let output = std::process::Command::new(binary_path) + .args(&args) + .output() + .map_err(|e| Error::Hwi(format!("Failed to execute command: {}", e), None))?; + if output.status.success() { + Ok(str::from_utf8(&output.stdout) + .map_err(|e| Error::Hwi(format!("Failed to parse HWI output: {}", e), None))? + .to_string()) + } else { + Err(Error::Hwi( + str::from_utf8(&output.stderr) + .unwrap_or("Failed to parse error output") + .to_string(), + None, + )) + } + } } - #[test] - #[serial] - #[ignore] - fn test_find_trezor_device() { - HWIClient::::find_device( - None, - Some(HWIDeviceType::Trezor), - None, - false, - Network::Testnet, - ) - .unwrap(); - } + macro_rules! generate_enumerate_test { + ($impl:ty) => { + #[test] + #[serial] + fn test_enumerate() { + let devices = HWIClient::<$impl>::enumerate().unwrap(); + assert!(!devices.is_empty()); + } - fn get_first_device() -> HWIClient { - let devices = HWIClient::::enumerate().unwrap(); - let device = devices - .first() - .expect("No devices found. Either plug in a hardware wallet, or start a simulator.") - .as_ref() - .expect("Error when opening the first device"); - HWIClient::::get_client(device, true, TESTNET).unwrap() - } + #[test] + #[serial] + #[ignore] + fn test_find_trezor_device() { + HWIClient::<$impl>::find_device( + None, + Some(HWIDeviceType::Trezor), + None, + false, + Network::Testnet, + ) + .unwrap(); + } - #[test] - #[serial] - fn test_get_master_xpub() { - let client = get_first_device(); - client - .get_master_xpub(types::HWIAddressType::Wit, 0) - .unwrap(); - } + fn get_first_device() -> HWIClient<$impl> { + let devices = HWIClient::<$impl>::enumerate().unwrap(); + let device = devices + .first() + .expect( + "No devices found. Either plug in a hardware wallet, or start a simulator.", + ) + .as_ref() + .expect("Error when opening the first device"); + HWIClient::get_client(device, true, TESTNET).unwrap() + } - #[test] - #[serial] - fn test_get_xpub() { - let client = get_first_device(); - let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); - client.get_xpub(&derivation_path, false).unwrap(); - } + #[test] + #[serial] + fn test_get_master_xpub() { + let client = get_first_device(); + client + .get_master_xpub(types::HWIAddressType::Wit, 0) + .unwrap(); + } - #[test] - #[serial] - fn test_sign_message() { - let client = get_first_device(); - let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); - client - .sign_message("I love BDK wallet", &derivation_path) - .unwrap(); - } + #[test] + #[serial] + fn test_get_xpub() { + let client = get_first_device(); + let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); + client.get_xpub(&derivation_path, false).unwrap(); + } - #[test] - #[serial] - fn test_get_string_descriptors() { - let client = get_first_device(); - let account = Some(10); - let descriptor = client.get_descriptors::(account).unwrap(); - assert!(!descriptor.internal.is_empty()); - assert!(!descriptor.receive.is_empty()); - } + #[test] + #[serial] + fn test_sign_message() { + let client = get_first_device(); + let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); + client + .sign_message("I love BDK wallet", &derivation_path) + .unwrap(); + } - #[test] - #[serial] - fn test_display_address_with_string_desc() { - let client = get_first_device(); - let descriptor = client.get_descriptors::(None).unwrap(); - let descriptor = descriptor.receive.first().unwrap(); - client.display_address_with_desc(descriptor).unwrap(); - } + #[test] + #[serial] + fn test_get_string_descriptors() { + let client = get_first_device(); + let account = Some(10); + let descriptor = client.get_descriptors::(account).unwrap(); + assert!(!descriptor.internal.is_empty()); + assert!(!descriptor.receive.is_empty()); + } - #[test] - #[serial] - #[cfg(feature = "miniscript")] - fn test_get_miniscript_descriptors() { - let client = get_first_device(); - let account = Some(10); - let descriptor = client - .get_descriptors::>(account) - .unwrap(); - assert!(!descriptor.internal.is_empty()); - assert!(!descriptor.receive.is_empty()); - } + #[test] + #[serial] + fn test_display_address_with_string_desc() { + let client = get_first_device(); + let descriptor = client.get_descriptors::(None).unwrap(); + let descriptor = descriptor.receive.first().unwrap(); + client.display_address_with_desc(descriptor).unwrap(); + } - #[test] - #[serial] - #[cfg(feature = "miniscript")] - fn test_display_address_with_miniscript_desc() { - let client = get_first_device(); - let descriptor = client - .get_descriptors::>(None) - .unwrap(); - let descriptor = descriptor.receive.first().unwrap(); - client.display_address_with_desc(descriptor).unwrap(); - } + #[test] + #[serial] + #[cfg(feature = "miniscript")] + fn test_get_miniscript_descriptors() { + let client = get_first_device(); + let account = Some(10); + let descriptor = client + .get_descriptors::>(account) + .unwrap(); + assert!(!descriptor.internal.is_empty()); + assert!(!descriptor.receive.is_empty()); + } - #[test] - #[serial] - fn test_display_address_with_path_legacy() { - let client = get_first_device(); - let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); - client - .display_address_with_path(&derivation_path, types::HWIAddressType::Legacy) - .unwrap(); - } + #[test] + #[serial] + #[cfg(feature = "miniscript")] + fn test_display_address_with_miniscript_desc() { + let client = get_first_device(); + let descriptor = client + .get_descriptors::>(None) + .unwrap(); + let descriptor = descriptor.receive.first().unwrap(); + client.display_address_with_desc(descriptor).unwrap(); + } - #[test] - #[serial] - fn test_display_address_with_path_nested_segwit() { - let client = get_first_device(); - let derivation_path = DerivationPath::from_str("m/49'/1'/0'/0/0").unwrap(); + #[test] + #[serial] + fn test_display_address_with_path_legacy() { + let client = get_first_device(); + let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); + client + .display_address_with_path(&derivation_path, types::HWIAddressType::Legacy) + .unwrap(); + } - client - .display_address_with_path(&derivation_path, types::HWIAddressType::Sh_Wit) - .unwrap(); - } + #[test] + #[serial] + fn test_display_address_with_path_nested_segwit() { + let client = get_first_device(); + let derivation_path = DerivationPath::from_str("m/49'/1'/0'/0/0").unwrap(); - #[test] - #[serial] - fn test_display_address_with_path_native_segwit() { - let client = get_first_device(); - let derivation_path = DerivationPath::from_str("m/84'/1'/0'/0/0").unwrap(); + client + .display_address_with_path(&derivation_path, types::HWIAddressType::Sh_Wit) + .unwrap(); + } - client - .display_address_with_path(&derivation_path, types::HWIAddressType::Wit) - .unwrap(); - } + #[test] + #[serial] + fn test_display_address_with_path_native_segwit() { + let client = get_first_device(); + let derivation_path = DerivationPath::from_str("m/84'/1'/0'/0/0").unwrap(); - // TODO: HWI 2.0.2 doesn't support displayaddress with taproot - // #[test] - // #[serial] - // fn test_display_address_with_path_taproot() {} + client + .display_address_with_path(&derivation_path, types::HWIAddressType::Wit) + .unwrap(); + } - // For testing on Coldcard, make sure to disable Max Network Fee in the Coldcard settings - #[test] - #[serial] - fn test_sign_tx() { - let devices = HWIClient::::enumerate().unwrap(); - let device = devices.first().unwrap().as_ref().unwrap(); - let client = - HWIClient::::get_client(device, true, TESTNET).unwrap(); - let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); - - let address = client - .display_address_with_path(&derivation_path, types::HWIAddressType::Legacy) - .unwrap(); - - let pk = client.get_xpub(&derivation_path, true).unwrap(); - let mut hd_keypaths: BTreeMap = Default::default(); - // Here device fingerprint is same as master xpub fingerprint - hd_keypaths.insert(pk.public_key, (device.fingerprint, derivation_path)); - - let script_pubkey = address.address.assume_checked().script_pubkey(); - - let previous_tx = Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::from_consensus(0), - input: vec![TxIn::default()], - output: vec![TxOut { - value: Amount::from_sat(100), - script_pubkey: script_pubkey.clone(), - }], - }; + // TODO: HWI 2.0.2 doesn't support displayaddress with taproot + // #[test] + // #[serial] + // fn test_display_address_with_path_taproot() {} + + // For testing on Coldcard, make sure to disable Max Network Fee in the Coldcard settings + #[test] + #[serial] + fn test_sign_tx() { + let devices = HWIClient::<$impl>::enumerate().unwrap(); + let device = devices.first().unwrap().as_ref().unwrap(); + let client = HWIClient::<$impl>::get_client(device, true, TESTNET).unwrap(); + let derivation_path = DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(); + + let address = client + .display_address_with_path(&derivation_path, types::HWIAddressType::Legacy) + .unwrap(); + + let pk = client.get_xpub(&derivation_path, true).unwrap(); + let mut hd_keypaths: BTreeMap = Default::default(); + // Here device fingerprint is same as master xpub fingerprint + hd_keypaths.insert(pk.public_key, (device.fingerprint, derivation_path)); + + let script_pubkey = address.address.assume_checked().script_pubkey(); + + let previous_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::from_consensus(0), + input: vec![TxIn::default()], + output: vec![TxOut { + value: Amount::from_sat(100), + script_pubkey: script_pubkey.clone(), + }], + }; + + let previous_txin = TxIn { + previous_output: bitcoin::OutPoint { + txid: previous_tx.compute_txid(), + vout: Default::default(), + }, + ..Default::default() + }; + let psbt = bitcoin::Psbt { + unsigned_tx: Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::from_consensus(0), + input: vec![previous_txin], + output: vec![TxOut { + value: Amount::from_sat(50), + script_pubkey, + }], + }, + xpub: Default::default(), + version: Default::default(), + proprietary: Default::default(), + unknown: Default::default(), + + inputs: vec![Input { + non_witness_utxo: Some(previous_tx), + witness_utxo: None, + bip32_derivation: hd_keypaths, + ..Default::default() + }], + outputs: vec![Output::default()], + }; + let client = get_first_device(); + client.sign_tx(&psbt).unwrap(); + } - let previous_txin = TxIn { - previous_output: bitcoin::OutPoint { - txid: previous_tx.compute_txid(), - vout: Default::default(), - }, - ..Default::default() - }; - let psbt = bitcoin::Psbt { - unsigned_tx: Transaction { - version: transaction::Version::ONE, - lock_time: absolute::LockTime::from_consensus(0), - input: vec![previous_txin], - output: vec![TxOut { - value: Amount::from_sat(50), - script_pubkey, - }], - }, - xpub: Default::default(), - version: Default::default(), - proprietary: Default::default(), - unknown: Default::default(), - - inputs: vec![Input { - non_witness_utxo: Some(previous_tx), - witness_utxo: None, - bip32_derivation: hd_keypaths, - ..Default::default() - }], - outputs: vec![Output::default()], - }; - let client = get_first_device(); - client.sign_tx(&psbt).unwrap(); - } + #[test] + #[serial] + fn test_get_keypool() { + let client = get_first_device(); + let keypool = true; + let internal = false; + let address_type = types::HWIAddressType::Legacy; + let account = Some(8); + let derivation_path = DerivationPath::from_str("m/44'/1'/0'").unwrap(); + let start = 1; + let end = 5; + client + .get_keypool( + keypool, + internal, + address_type, + false, + account, + Some(&derivation_path), + start, + end, + ) + .unwrap(); + + let keypool = true; + let internal = true; + let address_type = types::HWIAddressType::Wit; + let account = None; + let start = 1; + let end = 8; + client + .get_keypool( + keypool, + internal, + address_type, + false, + account, + None, + start, + end, + ) + .unwrap(); + + let keypool = false; + let internal = true; + let address_type = types::HWIAddressType::Sh_Wit; + let account = Some(1); + let start = 0; + let end = 10; + client + .get_keypool( + keypool, + internal, + address_type, + false, + account, + Some(&derivation_path), + start, + end, + ) + .unwrap(); + } - #[test] - #[serial] - fn test_get_keypool() { - let client = get_first_device(); - let keypool = true; - let internal = false; - let address_type = types::HWIAddressType::Legacy; - let account = Some(8); - let derivation_path = DerivationPath::from_str("m/44'/1'/0'").unwrap(); - let start = 1; - let end = 5; - client - .get_keypool( - keypool, - internal, - address_type, - false, - account, - Some(&derivation_path), - start, - end, - ) - .unwrap(); - - let keypool = true; - let internal = true; - let address_type = types::HWIAddressType::Wit; - let account = None; - let start = 1; - let end = 8; - client - .get_keypool( - keypool, - internal, - address_type, - false, - account, - None, - start, - end, - ) - .unwrap(); - - let keypool = false; - let internal = true; - let address_type = types::HWIAddressType::Sh_Wit; - let account = Some(1); - let start = 0; - let end = 10; - client - .get_keypool( - keypool, - internal, - address_type, - false, - account, - Some(&derivation_path), - start, - end, - ) - .unwrap(); - } + #[test] + #[serial] + #[ignore] + fn test_install_udev_rules() { + if cfg!(target_os = "linux") { + HWIClient::<$impl>::install_udev_rules(None, None).unwrap() + } + } - #[test] - #[serial] - #[ignore] - fn test_install_udev_rules() { - if cfg!(target_os = "linux") { - HWIClient::::install_udev_rules(None, None).unwrap() - } - } + #[test] + #[serial] + fn test_toggle_passphrase() { + let devices = HWIClient::<$impl>::enumerate().unwrap(); + let unsupported = [ + HWIDeviceType::Ledger, + HWIDeviceType::BitBox01, + HWIDeviceType::Coldcard, + HWIDeviceType::Jade, + ]; + for device in devices { + let device = device.unwrap(); + if unsupported.contains(&device.device_type) { + // These devices don't support togglepassphrase + continue; + } + let client = HWIClient::<$impl>::get_client(&device, true, TESTNET).unwrap(); + client.toggle_passphrase().unwrap(); + break; + } + } - #[test] - #[serial] - fn test_set_log_level() { - HWIClient::::set_log_level(types::LogLevel::DEBUG).unwrap(); - test_enumerate(); - } + #[test] + #[serial] + fn test_get_version() { + HWIClient::<$impl>::get_version().unwrap(); + } - #[test] - #[serial] - fn test_toggle_passphrase() { - let devices = HWIClient::::enumerate().unwrap(); - let unsupported = [ - HWIDeviceType::Ledger, - HWIDeviceType::BitBox01, - HWIDeviceType::Coldcard, - HWIDeviceType::Jade, - ]; - for device in devices { - let device = device.unwrap(); - if unsupported.contains(&device.device_type) { - // These devices don't support togglepassphrase - continue; + #[test] + #[serial] + #[ignore] + // At the moment (hwi v2.1.1 and trezor-firmware core v2.5.2) work only with physical devices and NOT emulators! + fn test_setup_trezor_device() { + let client = HWIClient::<$impl>::find_device( + None, + Some(HWIDeviceType::Trezor), + None, + false, + Network::Testnet, + ) + .unwrap(); + client.setup_device(Some("My Label"), None).unwrap(); } - let client = - HWIClient::::get_client(&device, true, TESTNET).unwrap(); - client.toggle_passphrase().unwrap(); - break; - } - } - #[test] - #[serial] - fn test_get_version() { - HWIClient::::get_version().unwrap(); - } + #[test] + #[serial] + #[ignore] + // At the moment (hwi v2.1.1 and trezor-firmware core v2.5.2) work only with physical devices and NOT emulators! + fn test_restore_trezor_device() { + let client = HWIClient::<$impl>::find_device( + None, + Some(HWIDeviceType::Trezor), + None, + false, + Network::Testnet, + ) + .unwrap(); + client.restore_device(Some("My Label"), None).unwrap(); + } - #[test] - #[serial] - #[ignore] - // At the moment (hwi v2.1.1 and trezor-firmware core v2.5.2) work only with physical devices and NOT emulators! - fn test_setup_trezor_device() { - let client = HWIClient::::find_device( - None, - Some(HWIDeviceType::Trezor), - None, - false, - Network::Testnet, - ) - .unwrap(); - client.setup_device(Some("My Label"), None).unwrap(); + #[test] + #[serial] + fn test_backup_device() { + let devices = HWIClient::<$impl>::enumerate().unwrap(); + let supported = [ + HWIDeviceType::BitBox01, + HWIDeviceType::BitBox02, + HWIDeviceType::Coldcard, + ]; + for device in devices { + let device = device.unwrap(); + if supported.contains(&device.device_type) { + let client = + HWIClient::<$impl>::get_client(&device, true, TESTNET).unwrap(); + client.backup_device(Some("My Label"), None).unwrap(); + } + } + } + + #[test] + #[serial] + #[ignore] + fn test_wipe_device() { + let devices = HWIClient::<$impl>::enumerate().unwrap(); + let unsupported = [ + HWIDeviceType::Ledger, + HWIDeviceType::Coldcard, + HWIDeviceType::Jade, + ]; + for device in devices { + let device = device.unwrap(); + if unsupported.contains(&device.device_type) { + // These devices don't support wipe + continue; + } + let client = HWIClient::<$impl>::get_client(&device, true, TESTNET).unwrap(); + client.wipe_device().unwrap(); + } + } + }; } - #[test] - #[serial] - #[ignore] - // At the moment (hwi v2.1.1 and trezor-firmware core v2.5.2) work only with physical devices and NOT emulators! - fn test_restore_trezor_device() { - let client = HWIClient::::find_device( - None, - Some(HWIDeviceType::Trezor), - None, - false, - Network::Testnet, - ) - .unwrap(); - client.restore_device(Some("My Label"), None).unwrap(); + mod python_tests { + use super::*; + generate_enumerate_test!(PythonHWIImplementation); } - #[test] - #[serial] - fn test_backup_device() { - let devices = HWIClient::::enumerate().unwrap(); - let supported = [ - HWIDeviceType::BitBox01, - HWIDeviceType::BitBox02, - HWIDeviceType::Coldcard, - ]; - for device in devices { - let device = device.unwrap(); - if supported.contains(&device.device_type) { - let client = - HWIClient::::get_client(&device, true, TESTNET) - .unwrap(); - client.backup_device(Some("My Label"), None).unwrap(); - } - } + mod binary_tests { + use super::*; + generate_enumerate_test!(BinaryHWIImplementation); } #[test] #[serial] - #[ignore] - fn test_wipe_device() { + fn test_set_log_level() { + HWIClient::::set_log_level(types::LogLevel::DEBUG).unwrap(); let devices = HWIClient::::enumerate().unwrap(); - let unsupported = [ - HWIDeviceType::Ledger, - HWIDeviceType::Coldcard, - HWIDeviceType::Jade, - ]; - for device in devices { - let device = device.unwrap(); - if unsupported.contains(&device.device_type) { - // These devices don't support wipe - continue; - } - let client = - HWIClient::::get_client(&device, true, TESTNET).unwrap(); - client.wipe_device().unwrap(); - } + assert!(!devices.is_empty()); } #[test] From 7e61b2ca64e21e717d7ca6f20c0cb8ab2571ed5d Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 30 Aug 2024 14:55:24 +0400 Subject: [PATCH 4/8] Run tests in a single thread --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d9ff3a..d7ae372 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,9 +79,9 @@ jobs: - name: Download and Install HWI run: ./get_hwi.sh - name: Test - run: cargo test --features ${{ matrix.rust.features }} + run: cargo test --features ${{ matrix.rust.features }} -- --test-threads=1 - name: Wipe - run: cargo test test_wipe_device -- --ignored + run: cargo test test_wipe_device -- --ignored --test-threads=1 test-readme-examples: runs-on: ubuntu-22.04 steps: From e99761b378fa2c3718e9903424e13de0ea40595e Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 30 Aug 2024 15:42:54 +0400 Subject: [PATCH 5/8] Restart simulator when testing wipe command twice --- .github/workflows/main.yml | 8 ++++-- src/lib.rs | 55 +++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d7ae372..b66045f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -80,8 +80,12 @@ jobs: run: ./get_hwi.sh - name: Test run: cargo test --features ${{ matrix.rust.features }} -- --test-threads=1 - - name: Wipe - run: cargo test test_wipe_device -- --ignored --test-threads=1 + - name: Wipe python + run: cargo test test_wipe_device_pyhton -- --ignored --test-threads=1 + - name: Restart simulator + run: docker restart simulator + - name: Wipe binary + run: cargo test test_wipe_device_binary -- --ignored --test-threads=1 test-readme-examples: runs-on: ubuntu-22.04 steps: diff --git a/src/lib.rs b/src/lib.rs index c9b2fb8..977a4d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ mod tests { use crate::error::Error; use crate::implementations::binary_implementation::BinaryHWIImplementation; use crate::implementations::python_implementation::PythonHWIImplementation; - use crate::types::{self, HWIBinaryExecutor, HWIDeviceType, TESTNET}; + use crate::types::{self, HWIBinaryExecutor, HWIDeviceType, HWIImplementation, TESTNET}; use crate::HWIClient; use std::collections::BTreeMap; use std::str::FromStr; @@ -447,27 +447,6 @@ mod tests { } } } - - #[test] - #[serial] - #[ignore] - fn test_wipe_device() { - let devices = HWIClient::<$impl>::enumerate().unwrap(); - let unsupported = [ - HWIDeviceType::Ledger, - HWIDeviceType::Coldcard, - HWIDeviceType::Jade, - ]; - for device in devices { - let device = device.unwrap(); - if unsupported.contains(&device.device_type) { - // These devices don't support wipe - continue; - } - let client = HWIClient::<$impl>::get_client(&device, true, TESTNET).unwrap(); - client.wipe_device().unwrap(); - } - } }; } @@ -495,4 +474,36 @@ mod tests { fn test_install_hwi() { HWIClient::::install_hwilib(Some("2.1.1")).unwrap(); } + + #[test] + #[serial] + #[ignore] + fn test_wipe_device_pyhton() { + wipe_device::(); + } + + #[test] + #[serial] + #[ignore] + fn test_wipe_device_binary() { + wipe_device::>(); + } + + fn wipe_device() { + let devices = HWIClient::::enumerate().unwrap(); + let unsupported = [ + HWIDeviceType::Ledger, + HWIDeviceType::Coldcard, + HWIDeviceType::Jade, + ]; + for device in devices { + let device = device.unwrap(); + if unsupported.contains(&device.device_type) { + // These devices don't support wipe + continue; + } + let client = HWIClient::::get_client(&device, true, TESTNET).unwrap(); + client.wipe_device().unwrap(); + } + } } From 7f0807e03ae6a33b473d68a1066e003241475720 Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 30 Aug 2024 22:57:45 +0400 Subject: [PATCH 6/8] Restart simulator between different implementation tests --- .github/workflows/main.yml | 9 +++++++-- src/lib.rs | 12 ++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b66045f..0661163 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -78,8 +78,13 @@ jobs: run: rustup update - name: Download and Install HWI run: ./get_hwi.sh - - name: Test - run: cargo test --features ${{ matrix.rust.features }} -- --test-threads=1 + - name: Test python + run: cargo test --features ${{ matrix.rust.features }} test_python -- --test-threads=1 + - name: Restart simulator + if: matrix.emulator.name == 'ledger' + run: docker restart simulator + - name: Test binary + run: cargo test --features ${{ matrix.rust.features }} test_binary -- --test-threads=1 - name: Wipe python run: cargo test test_wipe_device_pyhton -- --ignored --test-threads=1 - name: Restart simulator diff --git a/src/lib.rs b/src/lib.rs index 977a4d4..2181c50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -450,19 +450,19 @@ mod tests { }; } - mod python_tests { + mod test_python_tests { use super::*; generate_enumerate_test!(PythonHWIImplementation); } - mod binary_tests { + mod test_binary_tests { use super::*; generate_enumerate_test!(BinaryHWIImplementation); } #[test] #[serial] - fn test_set_log_level() { + fn test_python_set_log_level() { HWIClient::::set_log_level(types::LogLevel::DEBUG).unwrap(); let devices = HWIClient::::enumerate().unwrap(); assert!(!devices.is_empty()); @@ -471,21 +471,21 @@ mod tests { #[test] #[serial] #[ignore] - fn test_install_hwi() { + fn test_python_install_hwi() { HWIClient::::install_hwilib(Some("2.1.1")).unwrap(); } #[test] #[serial] #[ignore] - fn test_wipe_device_pyhton() { + fn test_python_wipe_device() { wipe_device::(); } #[test] #[serial] #[ignore] - fn test_wipe_device_binary() { + fn test_binary_wipe_device() { wipe_device::>(); } From fe0f1117c61d5c16e7f8d93622f15095a15051ec Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 30 Aug 2024 17:10:11 +0400 Subject: [PATCH 7/8] Add serialize to structs --- src/types.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/types.rs b/src/types.rs index c4bface..f2cf7e5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -11,7 +11,7 @@ use bitcoin::Psbt; use pyo3::types::PyModule; use pyo3::{IntoPy, PyObject}; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize}; #[cfg(feature = "miniscript")] use miniscript::{Descriptor, DescriptorPublicKey}; @@ -19,7 +19,7 @@ use pyo3::prelude::PyAnyMethods; use crate::error::{Error, ErrorCode}; -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWIExtendedPubKey { pub xpub: Xpub, } @@ -32,7 +32,7 @@ impl Deref for HWIExtendedPubKey { } } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWISignature { #[serde(deserialize_with = "from_b64")] pub signature: Vec, @@ -55,12 +55,12 @@ impl Deref for HWISignature { } } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWIAddress { pub address: Address, } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWIPartiallySignedTransaction { #[serde(deserialize_with = "deserialize_psbt")] pub psbt: Psbt, @@ -84,7 +84,7 @@ impl ToDescriptor for String {} #[cfg(feature = "miniscript")] impl ToDescriptor for Descriptor {} -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWIDescriptor where T: ToDescriptor, @@ -93,7 +93,7 @@ where pub receive: Vec, } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWIKeyPoolElement { pub desc: String, pub range: Vec, @@ -103,7 +103,7 @@ pub struct HWIKeyPoolElement { pub watchonly: bool, } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] #[allow(non_camel_case_types)] pub enum HWIAddressType { Legacy, @@ -138,7 +138,7 @@ impl IntoPy for HWIAddressType { } } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWIChain(bitcoin::Network); impl fmt::Display for HWIChain { @@ -186,7 +186,7 @@ pub const TESTNET: HWIChain = HWIChain(Network::Testnet); // Used internally to deserialize the result of `hwi enumerate`. This might // contain an `error`, when it does, it might not contain all the fields `HWIDevice` // is supposed to have - for this reason, they're all Option. -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub(crate) struct HWIDeviceInternal { #[serde(rename(deserialize = "type"))] pub device_type: Option, @@ -199,7 +199,7 @@ pub(crate) struct HWIDeviceInternal { pub code: Option, } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWIDevice { #[serde(rename(deserialize = "type"))] pub device_type: HWIDeviceType, @@ -238,7 +238,7 @@ impl TryFrom for HWIDevice { } } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] pub struct HWIStatus { pub success: bool, } @@ -256,7 +256,7 @@ impl From for Result<(), Error> { } } -#[derive(Clone, Eq, PartialEq, Debug, Deserialize)] +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum HWIDeviceType { Ledger, From b2c59ca877d5496776a0ee1ed94271607900757d Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 26 Sep 2024 13:58:29 +0400 Subject: [PATCH 8/8] Support Signer with the new HWIImplementation option --- README.md | 2 +- src/interface.rs | 12 ++++-------- src/lib.rs | 11 ++++++----- src/signer.rs | 19 +++++++++---------- src/types.rs | 11 ++++++++--- 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 9a492a4..5cdadd7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ pip install -r requirements.txt use bitcoin::Network; use bitcoin::bip32::DerivationPath; use hwi::error::Error; -use hwi::HWIClient; +use hwi::types::HWIClient; use hwi::implementations::python_implementation::PythonHWIImplementation; use std::str::FromStr; diff --git a/src/interface.rs b/src/interface.rs index f0504d0..aa2a727 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -8,7 +8,7 @@ use serde_json::value::Value; use crate::error::Error; use crate::types::{ - HWIAddress, HWIAddressType, HWIChain, HWIDescriptor, HWIDevice, HWIDeviceInternal, + HWIAddress, HWIAddressType, HWIChain, HWIClient, HWIDescriptor, HWIDevice, HWIDeviceInternal, HWIDeviceType, HWIExtendedPubKey, HWIImplementation, HWIKeyPoolElement, HWIPartiallySignedTransaction, HWISignature, HWIStatus, HWIWordCount, LogLevel, ToDescriptor, }; @@ -22,14 +22,10 @@ macro_rules! deserialize_obj { }}; } -pub struct HWIClient { - implementation: T, -} - impl HWIClient { /// Lists all HW devices currently connected. /// ```no_run - /// # use hwi::HWIClient; + /// # use hwi::types::HWIClient; /// # use hwi::implementations::python_implementation::PythonHWIImplementation; /// # use hwi::error::Error; /// # fn main() -> Result<(), Error> { @@ -54,7 +50,7 @@ impl HWIClient { /// /// Setting `expert` to `true` will enable additional output for some commands. /// ``` - /// # use hwi::HWIClient; + /// # use hwi::types::HWIClient; /// # use hwi::types::*; /// # use hwi::error::Error; /// # use hwi::implementations::python_implementation::PythonHWIImplementation; @@ -88,7 +84,7 @@ impl HWIClient { /// /// Setting `expert` to `true` will enable additional output for some commands. /// ```no_run - /// # use hwi::HWIClient; + /// # use hwi::types::HWIClient; /// # use hwi::types::*; /// # use hwi::error::Error; /// # use hwi::implementations::python_implementation::PythonHWIImplementation; diff --git a/src/lib.rs b/src/lib.rs index 349182c..70bf076 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,13 +8,12 @@ //! use bitcoin::bip32::{ChildNumber, DerivationPath}; //! use hwi::error::Error; //! use hwi::implementations::python_implementation::PythonHWIImplementation; -//! use hwi::interface::HWIClient; //! use hwi::types; //! use std::str::FromStr; //! //! fn main() -> Result<(), Error> { //! // Find information about devices -//! let mut devices = HWIClient::::enumerate()?; +//! let mut devices = types::HWIClient::::enumerate()?; //! if devices.is_empty() { //! panic!("No device found!"); //! } @@ -79,7 +78,6 @@ extern crate serial_test; extern crate core; -pub use interface::HWIClient; #[cfg(feature = "signer")] pub use signer::HWISigner; @@ -97,8 +95,9 @@ mod tests { use crate::error::Error; use crate::implementations::binary_implementation::BinaryHWIImplementation; use crate::implementations::python_implementation::PythonHWIImplementation; - use crate::types::{self, HWIBinaryExecutor, HWIDeviceType, HWIImplementation, TESTNET}; - use crate::HWIClient; + use crate::types::{ + self, HWIBinaryExecutor, HWIClient, HWIDeviceType, HWIImplementation, TESTNET, + }; use std::collections::BTreeMap; use std::str::FromStr; @@ -112,6 +111,8 @@ mod tests { #[cfg(feature = "miniscript")] use miniscript::{Descriptor, DescriptorPublicKey}; + + #[derive(Debug)] struct HWIBinaryExecutorImpl; impl HWIBinaryExecutor for HWIBinaryExecutorImpl { diff --git a/src/signer.rs b/src/signer.rs index c2854d7..e4497a1 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -3,8 +3,7 @@ use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1}; use bdk_wallet::bitcoin::Psbt; use crate::error::Error; -use crate::types::{HWIChain, HWIDevice}; -use crate::HWIClient; +use crate::types::{HWIChain, HWIClient, HWIDevice, HWIImplementation}; use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; @@ -12,29 +11,29 @@ use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner} /// Custom signer for Hardware Wallets /// /// This ignores `sign_options` and leaves the decisions up to the hardware wallet. -pub struct HWISigner { +pub struct HWISigner { fingerprint: Fingerprint, - client: HWIClient, + client: HWIClient, } -impl HWISigner { +impl HWISigner { /// Create an instance from the specified device and chain - pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { - let client = HWIClient::get_client(device, false, chain)?; - Ok(HWISigner { + pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { + let client = HWIClient::::get_client(device, false, chain)?; + Ok(Self { fingerprint: device.fingerprint, client, }) } } -impl SignerCommon for HWISigner { +impl SignerCommon for HWISigner { fn id(&self, _secp: &Secp256k1) -> SignerId { SignerId::Fingerprint(self.fingerprint) } } -impl TransactionSigner for HWISigner { +impl TransactionSigner for HWISigner { fn sign_transaction( &self, psbt: &mut Psbt, diff --git a/src/types.rs b/src/types.rs index f2cf7e5..5be19fe 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,6 @@ use core::fmt; use std::convert::TryFrom; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use std::ops::Deref; use std::str::FromStr; @@ -320,7 +320,7 @@ pub enum HWIWordCount { W24 = 24, } -pub trait HWIImplementation { +pub trait HWIImplementation: Debug + Send + Sync { fn enumerate() -> Result; fn get_client(device: &HWIDevice, expert: bool, chain: HWIChain) -> Result where @@ -368,6 +368,11 @@ pub trait HWIImplementation { fn install_hwilib(version: String) -> Result<(), Error>; } -pub trait HWIBinaryExecutor { +#[derive(Clone, Eq, PartialEq, Debug, Copy)] +pub struct HWIClient { + pub implementation: T, +} + +pub trait HWIBinaryExecutor: Debug + Send + Sync { fn execute_command(args: Vec) -> Result; }