diff --git a/Cargo.toml b/Cargo.toml index af56a43..e0a0cfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,11 @@ authors.workspace = true repository.workspace = true [features] -default = ["ledger", "specter", "coldcard", "bitbox"] +default = ["ledger", "specter", "coldcard", "bitbox", "jade"] bitbox = ["tokio", "hidapi", "bitbox-api", "regex"] coldcard = ["dep:coldcard", "regex"] specter = ["tokio", "tokio-serial", "serialport"] +jade = ["tokio", "tokio-serial", "serde", "serde_bytes", "serde_cbor", "serialport", "reqwest"] ledger = ["regex", "tokio", "ledger_bitcoin_client", "ledger-transport-hidapi", "ledger-apdu", "hidapi"] regex = ["dep:regex"] @@ -31,14 +32,20 @@ async-trait = "0.1.52" futures = "0.3" bitcoin = { version = "0.31", default-features = false, features = ["base64", "serde", "std"] } -# specter +# specter & jade tokio-serial = { version = "5.4.1", optional = true } -serialport = { version = "4.2", optional = true } +serialport = { version = "4.3", optional = true } -#bitbox +# jade +serde = { version = "1.0", features = ["derive"], optional = true } +serde_bytes = { version = "0.11.14", optional = true } +serde_cbor = { version = "0.11", optional = true } +reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] , optional = true} + +# bitbox bitbox-api = { version = "0.2.3", default-features = false, features = ["usb", "tokio", "multithreaded"], optional = true } -#coldcard +# coldcard coldcard = { version = "0.12.1", optional = true } # ledger @@ -46,12 +53,12 @@ ledger_bitcoin_client = { version = "0.4.1", optional = true } ledger-apdu = { version = "0.10", optional = true } ledger-transport-hidapi = { version = "0.10.0", optional = true } -#bitbox & ledger -hidapi = { version = "2.4.1", features = ["linux-static-hidraw"], default-features = false, optional = true } +# bitbox & ledger +hidapi = { version = "2.5.1", features = ["linux-static-hidraw"], default-features = false, optional = true } regex = { version = "1.6.0", optional = true } -# specter & ledger & bitbox -tokio = { version = "1.21.0", features = ["net", "time", "io-util", "sync"], optional = true } +# jade & specter & ledger & bitbox +tokio = { version = "1.21.0", features = ["net", "time", "io-util", "sync", "macros"], 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 3504b23..e55ecd1 100644 --- a/README.md +++ b/README.md @@ -6,21 +6,21 @@ Current **Minimum Supported Rust Version**: v1.65.0 /// HWI is the common Hardware Wallet Interface. #[async_trait] pub trait HWI: Debug { - /// Return the device kind + /// 0. Return the device kind fn device_kind(&self) -> DeviceKind; - /// Application version or OS version. + /// 1. Application version or OS version. async fn get_version(&self) -> Result; - /// Get master fingerprint. + /// 2. Get master fingerprint. async fn get_master_fingerprint(&self) -> Result; - /// Get the xpub with the given derivation path. + /// 3. Get the xpub with the given derivation path. async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result; - /// Register a new wallet policy + /// 4. Register a new wallet policy async fn register_wallet(&self, name: &str, policy: &str) -> Result, Error>; - /// Returns true if the wallet is registered - async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result; - /// Display an address on the device screen + /// 5. Returns true if the wallet is registered + async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result; + /// 6. Display an address on the device screen async fn display_address(&self, script: &AddressScript) -> Result<(), Error>; - /// Sign a partially signed bitcoin transaction (PSBT). + /// 7. Sign a partially signed bitcoin transaction (PSBT). async fn sign_tx(&self, tx: &mut Psbt) -> Result<(), Error>; } @@ -37,17 +37,16 @@ pub enum AddressScript { A Empty case means the method is unimplemented on the client or device side. -| | BitBox02[^1] | Coldcard[^2] | Ledger Nano S/S+[^3] | Specter[^4] | -|----------------------- |--------------|------------- |----------------------|-------------| -| get_version | | >= 6.2.1X | >= v2.1.2 | | -| get_master_fingerprint | >= v9.15.0 | >= 6.2.1X | >= v2.1.2 | >= v1.8.0 | -| get_extended_pubkey | >= v9.15.0 | >= 6.2.1X | >= v2.1.2 | >= v1.8.0 | -| register_wallet | >= v9.15.0 | >= 6.2.1X | >= v2.1.2 | >= v1.8.0 | -| is_wallet_registered | >= v9.15.0 | >= 6.2.1X | *check hmac presence | | -| display_address | >= v9.15.0 | >= 6.2.1X | >= v2.1.2 | | -| sign_tx | >= v9.15.0 | >= 6.2.1X | >= v2.1.2 | >= v1.8.0 | +| device | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| -------------------- | ---------- | ---------- | ---------- | ---------- | -------------------- | ---------- | ---------- | +| BitBox02[^1] | | >= v9.15.0 | >= v9.15.0 | >= v9.15.0 | >= v9.15.0 | >= v9.15.0 | >= v9.15.0 | +| Coldcard[^2] | >= v6.2.1X | >= v6.2.1X | >= v6.2.1X | >= v6.2.1X | >= v6.2.1X | >= v6.2.1X | >= v6.2.1X | +| Jade[^3] | >= v1.0.30 | >= v1.0.30 | >= v1.0.30 | >= v1.0.30 | | >= v1.0.30 | >= v1.0.30 | +| Ledger Nano S/S+[^4] | >= v2.1.2 | >= v2.1.2 | >= v2.1.2 | >= v2.1.2 | *check hmac presence | >= v2.1.2 | >= v2.1.2 | +| Specter[^5] | | >= v1.8.0 | >= v1.8.0 | >= v1.8.0 | | | >= v1.8.0 | [^1]: https://github.com/digitalbitbox/bitbox02-firmware [^2]: https://github.com/alfred-hodler/rust-coldcard -[^3]: https://github.com/LedgerHQ/app-bitcoin-new -[^4]: https://github.com/cryptoadvance/specter-diy +[^3]: https://github.com/Blockstream/Jade +[^4]: https://github.com/LedgerHQ/app-bitcoin-new +[^5]: https://github.com/cryptoadvance/specter-diy diff --git a/cli/Cargo.toml b/cli/Cargo.toml index abed163..7653fa3 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,5 +16,5 @@ path = "src/bin/hwi.rs" clap = { version = "4.4.7", features = ["derive"] } bitcoin = "0.31" hex = "0.4" -async-hwi = "0.0.14" +async-hwi = { path = ".." } tokio = { version = "1", features = ["macros", "net", "rt", "rt-multi-thread", "io-util", "sync"] } diff --git a/cli/src/bin/hwi.rs b/cli/src/bin/hwi.rs index e29da21..14c98ec 100644 --- a/cli/src/bin/hwi.rs +++ b/cli/src/bin/hwi.rs @@ -67,7 +67,7 @@ enum PsbtCommands { #[arg(long)] wallet_name: Option, #[arg(long)] - wallet_policy: String, + wallet_policy: Option, #[arg(long)] hmac: Option, }, @@ -113,7 +113,7 @@ async fn main() -> Result<(), Box> { args.network, Some(command::Wallet { name: wallet_name.as_ref(), - policy: &policy, + policy: Some(&policy), hmac: hmac.as_ref(), }), ) @@ -148,7 +148,7 @@ async fn main() -> Result<(), Box> { } Commands::Device(DeviceCommands::List) => { for device in command::list(args.network, None).await? { - eprint!("{}", device.get_master_fingerprint().await?,); + eprint!("{}", device.get_master_fingerprint().await?); eprint!(" {}", device.device_kind()); if let Ok(version) = device.get_version().await.map(|v| v.to_string()) { eprint!(" {}", version); @@ -187,9 +187,10 @@ async fn main() -> Result<(), Box> { } } let (name, policy) = match device.device_kind() { - DeviceKind::Ledger | DeviceKind::LedgerSimulator | DeviceKind::Coldcard => { - (name.clone().expect("name is required"), policy.clone()) - } + DeviceKind::Ledger + | DeviceKind::LedgerSimulator + | DeviceKind::Coldcard + | DeviceKind::Jade => (name.clone().expect("name is required"), policy.clone()), _ => ("".into(), policy.clone()), }; let res = device.is_wallet_registered(&name, &policy).await?; @@ -206,7 +207,7 @@ async fn main() -> Result<(), Box> { args.network, Some(command::Wallet { name: wallet_name.as_ref(), - policy: &wallet_policy, + policy: wallet_policy.as_ref(), hmac: hmac.as_ref(), }), ) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index cecc703..1c719f9 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -2,6 +2,7 @@ pub mod command { use async_hwi::{ bitbox::{api::runtime, BitBox02, PairingBitbox02WithLocalCache}, coldcard, + jade::{self, Jade}, ledger::{HidApi, Ledger, LedgerSimulator, TransportHID}, specter::{Specter, SpecterSimulator}, HWI, @@ -11,7 +12,7 @@ pub mod command { pub struct Wallet<'a> { pub name: Option<&'a String>, - pub policy: &'a String, + pub policy: Option<&'a String>, pub hmac: Option<&'a String>, } @@ -31,6 +32,25 @@ pub mod command { } } + match Jade::enumerate().await { + Err(e) => println!("{:?}", e), + Ok(devices) => { + for device in devices { + let device = device.with_network(network); + if let Ok(info) = device.get_info().await { + if info.jade_state == jade::api::JadeState::Locked { + if let Err(e) = device.auth().await { + eprintln!("auth {:?}", e); + continue; + } + } + + hws.push(device.into()); + } + } + } + } + if let Ok(device) = LedgerSimulator::try_connect().await { hws.push(device.into()); } @@ -48,8 +68,8 @@ pub mod command { { if let Ok((device, _)) = device.wait_confirm().await { let mut bb02 = BitBox02::from(device).with_network(network); - if let Some(ref wallet) = wallet { - bb02 = bb02.with_policy(wallet.policy)?; + if let Some(ref policy) = wallet.as_ref().map(|w| w.policy).flatten() { + bb02 = bb02.with_policy(policy)?; } hws.push(bb02.into()); } @@ -92,7 +112,9 @@ pub mod command { wallet .name .ok_or::>("ledger requires a wallet name".into())?, - wallet.policy, + wallet + .policy + .ok_or::>("ledger requires a wallet policy".into())?, hmac, )?; } diff --git a/src/bitbox.rs b/src/bitbox.rs index 525bd90..93b1e04 100644 --- a/src/bitbox.rs +++ b/src/bitbox.rs @@ -504,7 +504,7 @@ mod tests { use super::*; #[test] - fn test_extract_keys_and_template() { + fn test_extract_script_config_policy() { let policy = extract_script_config_policy("wsh(or_d(pk([f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/**),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/**),older(100))))").unwrap(); assert_eq!(2, policy.pubkeys.len()); assert_eq!( diff --git a/src/jade/api.rs b/src/jade/api.rs new file mode 100644 index 0000000..b06d587 --- /dev/null +++ b/src/jade/api.rs @@ -0,0 +1,181 @@ +/// See https://github.com/Blockstream/Jade/blob/master/docs/index.rst +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use super::{JadeError, TransportError}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Request<'a, T: Serialize> { + pub id: &'a str, + pub method: &'a str, + pub params: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EmptyRequest; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Response { + pub id: String, + pub seqlen: Option, + pub seqnum: Option, + pub result: Option, + pub error: Option, +} + +impl Response { + pub fn into_result(self) -> Result { + if let Some(e) = self.error { + return Err(JadeError::Rpc(e)); + } + + self.result + .ok_or_else(|| TransportError::NoErrorOrResult.into()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Error { + pub code: i32, + pub message: Option, + pub data: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetXpubParams<'a> { + pub network: &'a str, + pub path: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthUserParams<'a> { + pub network: &'a str, + pub epoch: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AuthUserResponse { + Authenticated(bool), + PinServerRequired { http_request: PinServerRequest }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PinServerRequest { + pub params: PinServerRequestParams, + #[serde(alias = "on-reply")] + pub onreply: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PinServerRequestParams { + pub urls: PinServerUrls, + pub method: String, + pub accept: String, + pub data: PinParams, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PinServerUrls { + Array(Vec), + Object { url: String, onion: String }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PinParams { + pub data: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetInfoResponse { + #[serde(alias = "JADE_VERSION")] + pub jade_version: String, + #[serde(alias = "JADE_STATE")] + pub jade_state: JadeState, + #[serde(alias = "JADE_NETWORKS")] + pub jade_networks: JadeNetworks, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JadeState { + #[serde(alias = "UNINIT")] + Uninit, + #[serde(alias = "UNSAVED")] + Unsaved, + #[serde(alias = "LOCKED")] + Locked, + #[serde(alias = "READY")] + Ready, + #[serde(alias = "TEMP")] + Temp, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JadeNetworks { + #[serde(alias = "MAIN")] + Main, + #[serde(alias = "TEST")] + Test, + #[serde(alias = "ALL")] + All, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DescriptorInfoResponse { + pub descriptor_len: u32, + pub num_datavalues: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetRegisteredDescriptorParams<'a> { + pub descriptor_name: &'a str, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetRegisteredDescriptorResponse { + pub descriptor_name: String, + pub descriptor: String, + pub datavalues: BTreeMap, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RegisterDescriptorParams<'a> { + pub network: &'a str, + pub descriptor_name: &'a str, + pub descriptor: String, + pub datavalues: BTreeMap, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DescriptorAddressParams<'a> { + pub network: &'a str, + pub branch: u32, + pub pointer: u32, + pub descriptor_name: &'a str, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SignPsbtParams<'a> { + pub network: &'a str, + #[serde(with = "serde_bytes")] + pub psbt: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetExtendedDataParams<'a> { + pub origid: &'a str, + pub orig: &'a str, + pub seqnum: u32, + pub seqlen: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ResponseBytes { + pub id: String, + pub seqlen: Option, + pub seqnum: Option, + #[serde(with = "serde_bytes")] + pub result: Option>, + pub error: Option, +} diff --git a/src/jade/mod.rs b/src/jade/mod.rs new file mode 100644 index 0000000..396fb2d --- /dev/null +++ b/src/jade/mod.rs @@ -0,0 +1,557 @@ +pub mod api; +pub mod pinserver; + +use std::{ + collections::BTreeMap, + fmt::Debug, + str::FromStr, + time::{SystemTime, UNIX_EPOCH}, +}; + +use serde::{de::DeserializeOwned, Serialize}; + +use bitcoin::{ + bip32::{DerivationPath, Fingerprint, Xpub}, + psbt::Psbt, + Network, +}; + +use serialport::{available_ports, SerialPort, SerialPortType}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio_serial::SerialPortBuilderExt; + +pub use tokio_serial::SerialStream; + +use crate::{parse_version, utils}; + +use super::{AddressScript, DeviceKind, Error as HWIError, HWI}; +use async_trait::async_trait; + +pub const JADE_NETWORK_MAINNET: &str = "mainnet"; +pub const JADE_NETWORK_TESTNET: &str = "testnet"; + +#[derive(Debug)] +pub struct Jade { + transport: T, + network: &'static str, + kind: DeviceKind, + descriptor_name: Option, +} + +impl Jade { + pub fn new(transport: T) -> Self { + Self { + transport, + network: JADE_NETWORK_MAINNET, + kind: DeviceKind::Jade, + descriptor_name: None, + } + } + + pub fn with_network(mut self, network: Network) -> Self { + if network == Network::Bitcoin { + self.network = JADE_NETWORK_MAINNET; + } else { + self.network = JADE_NETWORK_TESTNET; + } + self + } + + pub fn with_wallet(mut self, descriptor_name: String) -> Self { + self.descriptor_name = Some(descriptor_name); + self + } + + pub async fn ping(&self) -> Result<(), JadeError> { + let _res: u64 = self + .transport + .request("ping", Option::::None) + .await? + .into_result()?; + Ok(()) + } + + pub async fn get_info(&self) -> Result { + let info: api::GetInfoResponse = self + .transport + .request("get_version_info", Option::::None) + .await? + .into_result()?; + Ok(info) + } + + pub async fn get_registered_descriptors( + &self, + ) -> Result, HWIError> { + let descriptors: BTreeMap = self + .transport + .request( + "get_registered_descriptors", + Option::::None, + ) + .await? + .into_result()?; + Ok(descriptors) + } + + pub async fn get_registered_descriptor( + &self, + name: &str, + ) -> Result { + let registered: api::GetRegisteredDescriptorResponse = self + .transport + .request( + "get_registered_descriptor", + Some(api::GetRegisteredDescriptorParams { + descriptor_name: name, + }), + ) + .await? + .into_result()?; + Ok(registered) + } + + pub async fn auth(&self) -> Result<(), JadeError> { + let res: api::AuthUserResponse = self + .transport + .request( + "auth_user", + Some(api::AuthUserParams { + network: self.network, + epoch: SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|t| t.as_secs()) + .unwrap_or(0), + }), + ) + .await? + .into_result()?; + + if let api::AuthUserResponse::PinServerRequired { http_request } = res { + let client = pinserver::PinServerClient::new(); + let pin_params: api::PinParams = client.request(http_request.params).await?; + let handshake_completed: bool = self + .transport + .request("pin", Some(pin_params)) + .await? + .into_result()?; + if !handshake_completed { + return Err(JadeError::HandShakeRefused); + } + } + Ok(()) + } +} + +#[async_trait] +impl HWI for Jade { + fn device_kind(&self) -> DeviceKind { + self.kind + } + + async fn get_version(&self) -> Result { + let info = self.get_info().await?; + parse_version(&info.jade_version) + } + + async fn get_master_fingerprint(&self) -> Result { + let xpub = self.get_extended_pubkey(&DerivationPath::master()).await?; + Ok(xpub.fingerprint()) + } + + async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result { + let s: String = self + .transport + .request( + "get_xpub", + Some(api::GetXpubParams { + network: self.network, + path: path.to_u32_vec(), + }), + ) + .await? + .into_result()?; + let xpub = Xpub::from_str(&s).map_err(|e| HWIError::Device(e.to_string()))?; + Ok(xpub) + } + + async fn display_address(&self, script: &AddressScript) -> Result<(), HWIError> { + match (self.descriptor_name.as_ref(), script) { + (Some(descriptor_name), AddressScript::Miniscript { index, change }) => { + let _address: String = self + .transport + .request( + "get_receive_address", + Some(api::DescriptorAddressParams { + network: self.network, + branch: u32::from(*change), + pointer: *index, + descriptor_name, + }), + ) + .await? + .into_result()?; + Ok(()) + } + _ => Err(HWIError::UnimplementedMethod), + } + } + + async fn register_wallet( + &self, + name: &str, + policy: &str, + ) -> Result, HWIError> { + let (descriptor_template, keys) = utils::extract_keys_and_template::(policy)?; + let registered: bool = self + .transport + .request( + "register_descriptor", + Some(api::RegisterDescriptorParams { + network: self.network, + descriptor_name: name, + descriptor: descriptor_template, + datavalues: keys + .into_iter() + .enumerate() + .map(|(i, key)| (format!("@{}", i), key)) + .collect(), + }), + ) + .await? + .into_result()?; + if !registered { + Err(HWIError::UserRefused) + } else { + Ok(None) + } + } + + async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result { + let registered_descriptors = self.get_registered_descriptors().await?; + if !registered_descriptors.contains_key(name) { + return Ok(false); + } + + let registered = self.get_registered_descriptor(name).await?; + + let (descriptor_template, keys) = utils::extract_keys_and_template::(policy)?; + let datavalues: BTreeMap = keys + .into_iter() + .enumerate() + .map(|(i, key)| (format!("@{}", i), key)) + .collect(); + + Ok(registered.descriptor_name == name + && registered.descriptor == descriptor_template + && registered.datavalues == datavalues) + } + + async fn sign_tx(&self, psbt: &mut Psbt) -> Result<(), HWIError> { + let first: api::Response = self + .transport + .request( + "sign_psbt", + Some(api::SignPsbtParams { + network: self.network, + psbt: Psbt::serialize(psbt), + }), + ) + .await?; + + if let Some(e) = first.error { + return Err(JadeError::Rpc(e).into()); + } + + let mut psbt_bytes = first + .result + .ok_or(JadeError::Transport(TransportError::NoErrorOrResult))?; + + if let (Some(mut seqlen), Some(mut seqnum)) = (first.seqlen, first.seqnum) { + if seqlen > 1 { + while seqnum < seqlen { + let mut res: api::Response = self + .transport + .request( + "get_extended_data", + Some(api::GetExtendedDataParams { + origid: &first.id, + orig: "sign_psbt", + seqnum: seqnum + 1, + seqlen, + }), + ) + .await?; + + if let Some(e) = res.error { + return Err(JadeError::Rpc(e).into()); + } + + if let Some(bytes) = res.result.as_mut() { + psbt_bytes.append(bytes); + } else { + return Err(JadeError::Transport(TransportError::NoErrorOrResult).into()); + } + + if let (Some(len), Some(num)) = (res.seqlen, res.seqnum) { + seqlen = len; + seqnum = num; + } else { + return Err(JadeError::Transport(TransportError::NoErrorOrResult).into()); + } + } + } + } + + let signed_psbt = + Psbt::deserialize(&psbt_bytes).map_err(|e| HWIError::Device(e.to_string()))?; + utils::merge_signatures(psbt, &signed_psbt); + + Ok(()) + } +} + +impl From> for Box { + fn from(s: Jade) -> Box { + Box::new(s) + } +} + +async fn exchange( + transport: &mut T, + method: &str, + params: Option, +) -> Result, JadeError> +where + T: Unpin + AsyncRead + AsyncWrite, + S: Serialize + Unpin, + D: DeserializeOwned + Unpin, +{ + let (reader, mut writer) = tokio::io::split(transport); + + let id = std::process::id(); + let req = serde_cbor::to_vec(&api::Request { + id: &id.to_string(), + method, + params, + }) + .map_err(TransportError::from)?; + + writer.write_all(&req).await.map_err(TransportError::from)?; + + let response = read_stream(reader).await?; + + if response.id != id.to_string() { + return Err(TransportError::NonceMismatch.into()); + } + + Ok(response) +} + +async fn read_stream( + mut stream: S, +) -> Result, TransportError> { + let mut buf = Vec::::new(); + let mut chunk = [0; 1024]; + let n = stream.read(&mut chunk).await?; + buf.extend_from_slice(&chunk[..n]); + if let Ok(response) = serde_cbor::from_slice(&buf) { + return Ok(response); + } + loop { + tokio::select! { + res = stream.read(&mut chunk) => { + let n = res?; + if n == 0 { + break; + } + buf.extend_from_slice(&chunk[..n]); + if let Ok(response) = serde_cbor::from_slice(&buf) { + return Ok(response); + } + } + _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => { + break; + } + } + } + match serde_cbor::from_slice(&buf) { + Ok(response) => Ok(response), + Err(_) => Err(TransportError::NoErrorOrResult), + } +} + +#[async_trait] +pub trait Transport: Debug { + async fn request( + &self, + method: &str, + params: Option, + ) -> Result, JadeError>; +} + +impl Jade { + pub async fn enumerate() -> Result, JadeError> { + let mut res = Vec::new(); + for port_name in SerialTransport::enumerate_potential_ports()? { + let jade = Jade::::new(SerialTransport { port_name }); + jade.ping().await?; + res.push(jade); + } + Ok(res) + } +} + +#[derive(Debug)] +pub struct SerialTransport { + pub port_name: String, +} + +pub const JADE_DEVICE_IDS: [(u16, u16); 4] = [ + (0x10c4, 0xea60), + (0x1a86, 0x55d4), + (0x0403, 0x6001), + (0x1a86, 0x7523), +]; + +impl SerialTransport { + pub fn new(port_name: String) -> Self { + Self { port_name } + } + pub fn enumerate_potential_ports() -> Result, JadeError> { + match available_ports() { + Ok(ports) => Ok(ports + .into_iter() + .filter_map(|p| match p.port_type { + SerialPortType::PciPort => Some(p.port_name), + SerialPortType::UsbPort(info) => { + if JADE_DEVICE_IDS.contains(&(info.vid, info.pid)) { + Some(p.port_name) + } else { + None + } + } + _ => None, + }) + .collect()), + Err(e) => Err(JadeError::Transport(e.into())), + } + } +} + +const DEFAULT_JADE_BAUD_RATE: u32 = 115200; + +#[async_trait] +impl Transport for SerialTransport { + async fn request( + &self, + method: &str, + params: Option, + ) -> Result, JadeError> { + let mut transport = tokio_serial::new(self.port_name.clone(), DEFAULT_JADE_BAUD_RATE) + .open_native_async() + .map_err(|e| JadeError::Transport(e.into()))?; + // Ensure RTS and DTR are not set (as this can cause the hw to reboot) + // according to https://github.com/Blockstream/Jade/blob/master/jadepy/jade_serial.py#L56 + transport + .write_request_to_send(false) + .map_err(TransportError::from)?; + transport + .write_data_terminal_ready(false) + .map_err(TransportError::from)?; + exchange(&mut transport, method, params).await + } +} + +#[derive(Debug)] +pub enum TransportError { + Serialize(serde_cbor::Error), + NoErrorOrResult, + NonceMismatch, + Io(std::io::Error), + Serial(serialport::Error), +} + +impl From for TransportError { + fn from(e: serde_cbor::Error) -> Self { + Self::Serialize(e) + } +} + +impl From for TransportError { + fn from(e: std::io::Error) -> Self { + Self::Io(e) + } +} + +impl From for TransportError { + fn from(e: serialport::Error) -> Self { + Self::Serial(e) + } +} + +impl std::fmt::Display for TransportError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Serialize(e) => write!(f, "{}", e), + Self::NoErrorOrResult => write!(f, "No Error or Result"), + Self::NonceMismatch => write!(f, "Nonce mismatched"), + Self::Io(e) => write!(f, "{}", e), + Self::Serial(e) => write!(f, "{}", e), + } + } +} + +#[derive(Debug)] +pub enum JadeError { + DeviceNotFound, + DeviceDidNotSign, + UserCancelled, + Transport(TransportError), + Rpc(api::Error), + PinServer(pinserver::Error), + HandShakeRefused, +} + +impl From for JadeError { + fn from(e: TransportError) -> Self { + Self::Transport(e) + } +} + +impl From for JadeError { + fn from(e: pinserver::Error) -> Self { + Self::PinServer(e) + } +} + +impl std::fmt::Display for JadeError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::DeviceNotFound => write!(f, "Jade not found"), + Self::DeviceDidNotSign => write!(f, "Jade did not sign the psbt"), + Self::Transport(e) => write!(f, "{}", e), + Self::Rpc(e) => write!(f, "{:?}", e), + Self::UserCancelled => write!(f, "User cancelled operation"), + Self::PinServer(e) => write!(f, "{:?}", e), + Self::HandShakeRefused => write!(f, "Handshake with pinserver refused"), + } + } +} + +impl From for HWIError { + fn from(e: JadeError) -> HWIError { + match e { + JadeError::DeviceNotFound => HWIError::DeviceNotFound, + JadeError::DeviceDidNotSign => HWIError::DeviceDidNotSign, + JadeError::Transport(e) => HWIError::Device(e.to_string()), + JadeError::Rpc(e) => HWIError::Device(format!("{:?}", e)), + JadeError::PinServer(e) => HWIError::Device(format!("{:?}", e)), + JadeError::UserCancelled => HWIError::UserRefused, + JadeError::HandShakeRefused => { + HWIError::Device("Handshake with pinserver refused".to_string()) + } + } + } +} diff --git a/src/jade/pinserver.rs b/src/jade/pinserver.rs new file mode 100644 index 0000000..cc7325e --- /dev/null +++ b/src/jade/pinserver.rs @@ -0,0 +1,50 @@ +use super::api; + +pub struct PinServerClient { + pub client: reqwest::Client, +} + +impl Default for PinServerClient { + fn default() -> Self { + Self::new() + } +} + +impl PinServerClient { + pub fn new() -> Self { + Self { + client: reqwest::Client::new(), + } + } + + pub async fn request(&self, req: api::PinServerRequestParams) -> Result + where + D: serde::de::DeserializeOwned, + { + let url = match &req.urls { + api::PinServerUrls::Array(urls) => urls.first().ok_or(Error::NoUrlProvided)?, + api::PinServerUrls::Object { url, .. } => url, + }; + + let res = self.client.post(url).json(&req.data).send().await?; + + if res.status().is_success() { + res.json().await.map_err(Error::from) + } else { + Err(Error::Server(format!("{:?}", res))) + } + } +} + +#[derive(Debug)] +pub enum Error { + NoUrlProvided, + Client(reqwest::Error), + Server(String), +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Self::Client(e) + } +} diff --git a/src/ledger.rs b/src/ledger.rs index 92d1b73..00a6f2b 100644 --- a/src/ledger.rs +++ b/src/ledger.rs @@ -2,7 +2,6 @@ use std::convert::TryFrom; use std::default::Default; use std::error::Error; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; use async_trait::async_trait; use bitcoin::{ @@ -10,7 +9,6 @@ use bitcoin::{ psbt::Psbt, }; use ledger_bitcoin_client::psbt::PartialSignature; -use regex::Regex; use ledger_apdu::APDUAnswer; use ledger_transport_hidapi::TransportNativeHID; @@ -57,7 +55,7 @@ impl Ledger { policy: &str, hmac: Option<[u8; 32]>, ) -> Result { - let (descriptor_template, keys) = extract_keys_and_template(policy)?; + let (descriptor_template, keys) = utils::extract_keys_and_template::(policy)?; let wallet = WalletPolicy::new(name.into(), WalletVersion::V2, descriptor_template, keys); self.options.wallet = Some((wallet, hmac)); Ok(self) @@ -113,7 +111,8 @@ impl HWI for Ledger { path.to_string().trim_start_matches('m'), xpub ); - let (descriptor_template, keys) = extract_keys_and_template(&policy)?; + let (descriptor_template, keys) = + utils::extract_keys_and_template::(&policy)?; let wallet = WalletPolicy::new("".into(), WalletVersion::V2, descriptor_template, keys); @@ -146,7 +145,7 @@ impl HWI for Ledger { name: &str, policy: &str, ) -> Result, HWIError> { - let (descriptor_template, keys) = extract_keys_and_template(policy)?; + let (descriptor_template, keys) = utils::extract_keys_and_template::(policy)?; let wallet = WalletPolicy::new( name.to_string(), WalletVersion::V2, @@ -159,7 +158,8 @@ impl HWI for Ledger { async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result { if let Some((wallet, hmac)) = &self.options.wallet { - let (descriptor_template, keys) = extract_keys_and_template(policy)?; + let (descriptor_template, keys) = + utils::extract_keys_and_template::(policy)?; Ok(hmac.is_some() && name == wallet.name && descriptor_template == wallet.descriptor_template @@ -194,31 +194,6 @@ impl HWI for Ledger { } } -pub fn extract_keys_and_template(policy: &str) -> Result<(String, Vec), HWIError> { - let re = Regex::new(r"((\[.+?\])?[xyYzZtuUvV]pub[1-9A-HJ-NP-Za-km-z]{79,108})").unwrap(); - let mut descriptor_template = policy.to_string(); - let mut pubkeys_str: Vec<&str> = Vec::new(); - for capture in re.find_iter(policy) { - if !pubkeys_str.contains(&capture.as_str()) { - pubkeys_str.push(capture.as_str()); - } - } - - let mut pubkeys: Vec = Vec::new(); - for (i, key_str) in pubkeys_str.iter().enumerate() { - descriptor_template = descriptor_template.replace(key_str, &format!("@{}", i)); - let pubkey = WalletPubKey::from_str(key_str).map_err(|_| HWIError::UnsupportedInput)?; - pubkeys.push(pubkey); - } - - // Do not include the hash in the descriptor template. - if let Some((descriptor_template, _hash)) = descriptor_template.rsplit_once('#') { - Ok((descriptor_template.to_string(), pubkeys)) - } else { - Ok((descriptor_template, pubkeys)) - } -} - impl Ledger { pub fn enumerate(api: &HidApi) -> impl Iterator { TransportNativeHID::list_ledgers(api) @@ -338,24 +313,3 @@ impl From> for HWIError { HWIError::Device(format!("{:#?}", e)) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_extract_keys_and_template() { - let res = extract_keys_and_template("wsh(or_d(pk([f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/**),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/**),older(100))))").unwrap(); - assert_eq!(res.0, "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/**),older(100))))"); - assert_eq!(res.1.len(), 2); - assert_eq!(res.1[0].to_string(), "[f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP".to_string()); - assert_eq!(res.1[1].to_string(), "tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S".to_string()); - - let res = extract_keys_and_template("wsh(or_d(multi(2,[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<0;1>/*,[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<0;1>/*),and_v(v:thresh(2,pkh([b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<2;3>/*),a:pkh([7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<2;3>/*),a:pkh([1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo/<0;1>/*)),older(300))))#wp0w3hlw").unwrap(); - assert_eq!(res.0, "wsh(or_d(multi(2,@0/<0;1>/*,@1/<0;1>/*),and_v(v:thresh(2,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/<0;1>/*)),older(300))))"); - assert_eq!(res.1.len(), 3); - assert_eq!(res.1[0].to_string(), "[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ".to_string()); - assert_eq!(res.1[1].to_string(), "[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ".to_string()); - assert_eq!(res.1[2].to_string(), "[1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo".to_string()); - } -} diff --git a/src/lib.rs b/src/lib.rs index 6387e3b..e6bde69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ pub mod bip389; pub mod bitbox; #[cfg(feature = "coldcard")] pub mod coldcard; +#[cfg(feature = "jade")] +pub mod jade; #[cfg(feature = "ledger")] pub mod ledger; #[cfg(feature = "specter")] @@ -175,6 +177,7 @@ pub enum DeviceKind { SpecterSimulator, Ledger, LedgerSimulator, + Jade, } impl std::fmt::Display for DeviceKind { @@ -186,6 +189,7 @@ impl std::fmt::Display for DeviceKind { DeviceKind::SpecterSimulator => write!(f, "specter-simulator"), DeviceKind::Ledger => write!(f, "ledger"), DeviceKind::LedgerSimulator => write!(f, "ledger-simulator"), + DeviceKind::Jade => write!(f, "jade"), } } } @@ -200,6 +204,7 @@ impl std::str::FromStr for DeviceKind { "specter-simulator" => Ok(DeviceKind::SpecterSimulator), "ledger" => Ok(DeviceKind::Ledger), "ledger-simulator" => Ok(DeviceKind::LedgerSimulator), + "jade" => Ok(DeviceKind::Jade), _ => Err(()), } } diff --git a/src/utils.rs b/src/utils.rs index 24ed1f9..00d3194 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, collections::BTreeMap}; +use std::{cmp::Ordering, collections::BTreeMap, str::FromStr}; use bitcoin::{ bip32::{ChildNumber, DerivationPath, KeySource}, @@ -90,6 +90,28 @@ impl<'a> Bip32DerivationFilter<'a> { } } +pub fn merge_signatures(psbt: &mut Psbt, signed_psbt: &Psbt) { + for i in 0..signed_psbt.inputs.len() { + let psbtin = match psbt.inputs.get_mut(i) { + Some(psbtin) => psbtin, + None => continue, + }; + let signed_psbtin = match signed_psbt.inputs.get(i) { + Some(signed_psbtin) => signed_psbtin, + None => continue, + }; + psbtin + .partial_sigs + .extend(&mut signed_psbtin.partial_sigs.iter()); + psbtin + .tap_script_sigs + .extend(&mut signed_psbtin.tap_script_sigs.iter()); + if let Some(sig) = signed_psbtin.tap_key_sig { + psbtin.tap_key_sig = Some(sig); + } + } +} + pub fn bip86_path_child_numbers(path: DerivationPath) -> Result, Error> { let children: Vec = path.into(); if children.len() != 5 @@ -108,6 +130,32 @@ pub fn bip86_path_child_numbers(path: DerivationPath) -> Result } } +#[cfg(feature = "regex")] +pub fn extract_keys_and_template(policy: &str) -> Result<(String, Vec), Error> { + let re = regex::Regex::new(r"((\[.+?\])?[xyYzZtuUvV]pub[1-9A-HJ-NP-Za-km-z]{79,108})").unwrap(); + let mut descriptor_template = policy.to_string(); + let mut pubkeys_str: Vec<&str> = Vec::new(); + for capture in re.find_iter(policy) { + if !pubkeys_str.contains(&capture.as_str()) { + pubkeys_str.push(capture.as_str()); + } + } + + let mut pubkeys: Vec = Vec::new(); + for (i, key_str) in pubkeys_str.iter().enumerate() { + descriptor_template = descriptor_template.replace(key_str, &format!("@{}", i)); + let pubkey = T::from_str(key_str).map_err(|_| Error::UnsupportedInput)?; + pubkeys.push(pubkey); + } + + // Do not include the hash in the descriptor template. + if let Some((descriptor_template, _hash)) = descriptor_template.rsplit_once('#') { + Ok((descriptor_template.to_string(), pubkeys)) + } else { + Ok((descriptor_template, pubkeys)) + } +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -125,4 +173,20 @@ mod tests { assert_eq!(psbt.inputs[0].bip32_derivation.len(), 2); assert_eq!(psbt.inputs[1].bip32_derivation.len(), 2); } + + #[test] + fn test_extract_keys_and_template() { + let res = extract_keys_and_template::("wsh(or_d(pk([f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/**),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/**),older(100))))").unwrap(); + assert_eq!(res.0, "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/**),older(100))))"); + assert_eq!(res.1.len(), 2); + assert_eq!(res.1[0], "[f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP".to_string()); + assert_eq!(res.1[1], "tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S".to_string()); + + let res = extract_keys_and_template::("wsh(or_d(multi(2,[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<0;1>/*,[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<0;1>/*),and_v(v:thresh(2,pkh([b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<2;3>/*),a:pkh([7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<2;3>/*),a:pkh([1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo/<0;1>/*)),older(300))))#wp0w3hlw").unwrap(); + assert_eq!(res.0, "wsh(or_d(multi(2,@0/<0;1>/*,@1/<0;1>/*),and_v(v:thresh(2,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/<0;1>/*)),older(300))))"); + assert_eq!(res.1.len(), 3); + assert_eq!(res.1[0], "[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ".to_string()); + assert_eq!(res.1[1], "[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ".to_string()); + assert_eq!(res.1[2], "[1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo".to_string()); + } }