diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c69a22..40f7c84 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: - name: clippy run: | sudo apt-get update && - sudo apt-get install libudev-dev pkg-config && + sudo apt-get install libudev-dev pkg-config protobuf-compiler && cargo clippy --all-features --all-targets -- -D warnings unit_tests: @@ -41,6 +41,14 @@ jobs: toolchain: ${{ matrix.toolchain }} override: true profile: minimal + - name: Install protobuf + if: matrix.os == 'macOS-latest' + run: | + brew install protobuf + - name: Install protobuf + if: matrix.os == 'windows-latest' + run: | + choco install protoc - name: Test on Rust ${{ matrix.toolchain }} if: matrix.os == 'windows-latest' || matrix.os == 'macOS-latest' run: cargo test --verbose --color always -- --nocapture @@ -48,5 +56,5 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update && - sudo apt-get install libudev-dev pkg-config && + sudo apt-get install libudev-dev pkg-config protobuf-compiler && cargo test --verbose --color always -- --nocapture diff --git a/Cargo.toml b/Cargo.toml index 161e025..cc3d3fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ repository = "https://github.com/wizardsardine/async-hwi" keywords = ["bitcoin"] [features] -default = ["ledger", "specter", "bitbox"] +default = ["ledger", "specter", "trezor"] +trezor = ["trezor-client"] bitbox = ["tokio", "hidapi", "bitbox-api", "regex" ] specter = ["tokio", "tokio-serial", "serialport"] ledger = ["regex", "tokio", "ledger_bitcoin_client", "ledger-transport-hidapi", "ledger-apdu", "hidapi"] @@ -39,5 +40,8 @@ regex = { version = "1.6.0", optional = true } # specter & ledger & bitbox tokio = { version = "1.21.0", features = ["net", "time", "io-util", "sync"], optional = true } +# trezor +trezor-client = { version = "0.1.2", optional = true } + [dev-dependencies] tokio = { version = "1.21", features = ["macros", "net", "rt", "rt-multi-thread", "io-util", "sync"] } diff --git a/README.md b/README.md index 2033c62..a90bb68 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,4 @@ pub trait HWI: Debug { | [Specter](https://github.com/cryptoadvance/specter-diy) | v1.8.0 | | [Ledger](https://github.com/LedgerHQ/app-bitcoin-new) | v2.1.2 | | [BitBox02](https://github.com/digitalbitbox/bitbox02-firmware) | v9.15.0 | - +| [Trezor](https://github.com/trezor/trezor-firmware) | all | diff --git a/examples/hwi.rs b/examples/hwi.rs index e3d6d8c..5f67c9a 100644 --- a/examples/hwi.rs +++ b/examples/hwi.rs @@ -6,6 +6,9 @@ use async_hwi::specter::{Specter, SpecterSimulator}; #[cfg(feature = "ledger")] use async_hwi::ledger::{HidApi, Ledger, LedgerSimulator, TransportHID}; +#[cfg(feature = "trezor")] +use async_hwi::trezor::TrezorClient; + #[tokio::main] pub async fn main() { let list = list_hardware_wallets().await; @@ -58,5 +61,13 @@ pub async fn list_hardware_wallets() -> Vec> { } } + #[cfg(feature = "trezor")] + { + let client = TrezorClient::connect_first(false).unwrap(); + if client.is_connected().await.is_ok() { + hws.push(client.into()); + } + } + hws } diff --git a/src/lib.rs b/src/lib.rs index 2c1d01d..94e0e1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ pub mod bitbox; pub mod ledger; #[cfg(feature = "specter")] pub mod specter; +#[cfg(feature = "trezor")] +pub mod trezor; use async_trait::async_trait; use bitcoin::{ @@ -90,6 +92,8 @@ pub enum DeviceKind { SpecterSimulator, Ledger, LedgerSimulator, + Trezor, + TrezorSimulator, } impl std::fmt::Display for DeviceKind { @@ -100,6 +104,8 @@ impl std::fmt::Display for DeviceKind { DeviceKind::SpecterSimulator => write!(f, "specter-simulator"), DeviceKind::Ledger => write!(f, "ledger"), DeviceKind::LedgerSimulator => write!(f, "ledger-simulator"), + DeviceKind::Trezor => write!(f, "trezor"), + DeviceKind::TrezorSimulator => write!(f, "trezor-simulator"), } } } @@ -114,6 +120,8 @@ impl std::str::FromStr for DeviceKind { "specter-simulator" => Ok(DeviceKind::SpecterSimulator), "ledger" => Ok(DeviceKind::Ledger), "ledger-simulator" => Ok(DeviceKind::LedgerSimulator), + "trezor" => Ok(DeviceKind::Trezor), + "trezor-simulator" => Ok(DeviceKind::TrezorSimulator), _ => Err(()), } } diff --git a/src/trezor.rs b/src/trezor.rs new file mode 100644 index 0000000..463e93b --- /dev/null +++ b/src/trezor.rs @@ -0,0 +1,231 @@ +//! # Examples +//! ```no_run +//! use async_hwi::trezor::TrezorClient; +//! use async_hwi::HWI; +//! +//! #[tokio::main] +//! pub async fn main() { +//! let mut hwi = TrezorClient::connect_first(false).unwrap(); +//! hwi.set_network(bitcoin::Network::Bitcoin); +//! println!("{}", hwi.get_version().await.unwrap()); +//! println!("{:?}", hwi.get_master_fingerprint().await); +//! let path = +//! ::from_str("m/44'/1'/0'/0/0") +//! .expect("Failed to parse path"); +//! println!("{:?}", hwi.get_extended_pubkey(&path).await); +//!} +//! ``` + +use std::{ + collections::HashMap, + str::FromStr, + sync::{Arc, Mutex}, +}; + +use async_trait::async_trait; +use bitcoin::{ + bip32::{DerivationPath, ExtendedPubKey, Fingerprint}, + ecdsa, + psbt::Psbt, + PublicKey, +}; +use trezor_client::{Trezor, TrezorResponse}; + +use crate::{DeviceKind, Error, HWI}; + +pub struct TrezorClient { + client: Arc>, + kind: DeviceKind, + network: bitcoin::Network, +} + +impl From for Box { + fn from(s: TrezorClient) -> Box { + Box::new(s) + } +} + +impl std::fmt::Debug for TrezorClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TrezorClient") + .field("client", &self.client.lock().unwrap().model()) + .finish() + } +} + +impl TrezorClient { + fn new(client: Trezor) -> Self { + let kind = match client.model() { + trezor_client::Model::TrezorEmulator => DeviceKind::TrezorSimulator, + _ => DeviceKind::Trezor, + }; + Self { + client: Arc::new(Mutex::new(client)), + kind, + network: bitcoin::Network::Testnet, + } + } + + pub fn connect_first(debug: bool) -> Result { + let mut devices = trezor_client::find_devices(debug); + if !devices.is_empty() { + let mut client = devices.remove(0).connect()?; + client.init_device(None)?; + Ok(Self::new(client)) + } else { + Err(Error::DeviceNotFound) + } + } + + pub fn get_simulator() -> Trezor { + let mut emulator = trezor_client::find_devices(false) + .into_iter() + .find(|t| t.model == trezor_client::Model::TrezorEmulator) + .expect("No emulator found") + .connect() + .expect("Failed to connect to emulator"); + emulator + .init_device(None) + .expect("Failed to intialize device"); + emulator + } + + pub fn get_network(&self) -> bitcoin::Network { + self.network + } + + pub fn set_network(&mut self, network: bitcoin::Network) { + self.network = network; + } +} + +#[async_trait] +impl HWI for TrezorClient { + fn device_kind(&self) -> crate::DeviceKind { + self.kind + } + + async fn get_version(&self) -> Result { + let client = self.client.lock().unwrap(); + let f = client.features(); + if let Some(f) = f { + let version = super::Version { + major: f.major_version(), + minor: f.minor_version(), + patch: f.patch_version(), + prerelease: None, + }; + Ok(version) + } else { + return Err(Error::Device(String::from("No features found"))); + } + } + + async fn is_connected(&self) -> Result<(), Error> { + match self.client.lock().unwrap().ping("PINGPING")? { + TrezorResponse::Ok(_) => Ok(()), + _ => Err(Error::DeviceDisconnected), + } + } + + async fn get_master_fingerprint(&self) -> Result { + let path = DerivationPath::default(); + match self.client.lock().unwrap().get_public_key( + &path, + trezor_client::InputScriptType::SPENDADDRESS, + self.network, + false, + ) { + Ok(TrezorResponse::Ok(key)) => { + let fp = key.fingerprint(); + Ok(fp) + } + Ok(TrezorResponse::Failure(f)) => Err(Error::Device(f.to_string())), + Ok(result) => Err(Error::Device(result.to_string())), + Err(e) => Err(Error::Device(e.to_string())), + } + } + + async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result { + let path = DerivationPath::from_str(&path.to_string()) + .map_err(|e| Error::Device(format!("{:?}", e)))?; + match self.client.lock().unwrap().get_public_key( + &path, + trezor_client::InputScriptType::SPENDADDRESS, + self.network, + false, + ) { + Ok(TrezorResponse::Ok(key)) => return Ok(key), + Ok(TrezorResponse::Failure(f)) => Err(Error::Device(f.to_string())), + Ok(result) => Err(Error::Device(result.to_string())), + Err(e) => Err(Error::Device(e.to_string())), + } + } + + async fn register_wallet(&self, _name: &str, _policy: &str) -> Result, Error> { + return Err(Error::UnimplementedMethod); + } + + async fn sign_tx(&self, tx: &mut Psbt) -> Result<(), Error> { + let master_fp = self.get_master_fingerprint().await?; + let mut signatures = HashMap::new(); + let mut client = self.client.lock().unwrap(); + let mut result = client.sign_tx(tx, self.network)?; + + // TODO: make this loop more elegant + // This could be done asynchrnously + loop { + match result { + TrezorResponse::Ok(progress) => { + if progress.has_signature() { + let (index, signature) = progress.get_signature().unwrap(); + let mut signature = signature.to_vec(); + // TODO: add support for multisig + signature.push(0x01); // Signature type + if signatures.contains_key(&index) { + return Err(Error::Device(format!( + "Signature for index {} already filled", + index + ))); + } + let val = ecdsa::Signature::from_slice(&signature) + .map_err(|e| Error::Device(format!("{:?}", e))); + signatures.insert(index, val?); + } + if progress.finished() { + for (index, input) in tx.inputs.iter_mut().enumerate() { + let signature = signatures.remove(&index).ok_or(Error::Device( + format!("Signature for index {} not found", index), + ))?; + for (pk, (fp, _)) in input.bip32_derivation.iter() { + let pk = PublicKey::from_slice(pk.serialize().as_ref()).unwrap(); + if *fp == master_fp { + input.partial_sigs.insert(pk, signature); + break; + } + } + } + return Ok(()); + } else { + result = progress.ack_psbt(tx, self.network)?; + } + } + TrezorResponse::Failure(f) => { + return Err(Error::Device(f.to_string())); + } + TrezorResponse::ButtonRequest(req) => { + result = req.ack()?; + } + _ => { + return Err(Error::Device(result.to_string())); + } + } + } + } +} + +impl From for Error { + fn from(value: trezor_client::Error) -> Self { + Error::Device(format!("{:#?}", value)) + } +}