From bd14db95e9d46d9606d7c8e9a60d6b4c23e89420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 7 Aug 2024 16:41:34 +0100 Subject: [PATCH 1/5] cli: add `--device-transport` arg to `Tx`, `KeyDerive`, SignGenesisTxs` --- crates/apps_lib/src/cli.rs | 31 +++++++++++++++++-- .../src/config/genesis/transactions.rs | 3 +- crates/sdk/src/args.rs | 29 +++++++++++++++++ crates/sdk/src/lib.rs | 4 ++- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index cc7c51844c..552fc4ef45 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -3469,6 +3469,8 @@ pub mod args { pub const WITH_INDEXER: ArgOpt = arg_opt("with-indexer"); pub const TX_PATH: Arg = arg("tx-path"); pub const TX_PATH_OPT: ArgOpt = TX_PATH.opt(); + pub const DEVICE_TRANSPORT: ArgDefault = + arg_default("device-transport", DefaultFn(DeviceTransport::default)); /// Global command arguments #[derive(Clone, Debug)] @@ -7034,6 +7036,7 @@ pub mod args { wrapper_fee_payer: self.wrapper_fee_payer.map(|x| ctx.get(&x)), memo: self.memo, use_device: self.use_device, + device_transport: self.device_transport, }) } } @@ -7164,6 +7167,15 @@ pub mod args { )) .conflicts_with(DRY_RUN_TX.name), ) + .arg( + DEVICE_TRANSPORT + .def() + .help(wrap!( + "Select transport for hardware wallet from \"hid\" \ + (default) or \"tcp\"." + )) + .conflicts_with(DRY_RUN_TX.name), + ) .arg( MEMO_OPT .def() @@ -7205,6 +7217,7 @@ pub mod args { None => TxExpiration::Default, } }; + let device_transport = DEVICE_TRANSPORT.parse(matches); Self { dry_run, dry_run_wrapper, @@ -7228,6 +7241,7 @@ pub mod args { output_folder, memo, use_device, + device_transport, } } } @@ -7357,22 +7371,24 @@ pub mod args { let alias = ALIAS.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); - let use_device = USE_DEVICE.parse(matches); let derivation_path = HD_DERIVATION_PATH.parse(matches); let allow_non_compliant = HD_ALLOW_NON_COMPLIANT_DERIVATION_PATH.parse(matches); let prompt_bip39_passphrase = HD_PROMPT_BIP39_PASSPHRASE.parse(matches); + let use_device = USE_DEVICE.parse(matches); + let device_transport = DEVICE_TRANSPORT.parse(matches); Self { scheme, shielded, alias, alias_force, unsafe_dont_encrypt, - use_device, derivation_path, allow_non_compliant, prompt_bip39_passphrase, + use_device, + device_transport, } } @@ -7402,6 +7418,10 @@ pub mod args { "Derive an address and public key from the seed stored on the \ connected hardware wallet." ))) + .arg(DEVICE_TRANSPORT.def().help(wrap!( + "Select transport for hardware wallet from \"hid\" (default) \ + or \"tcp\"." + ))) .arg(HD_DERIVATION_PATH.def().help(wrap!( "HD key derivation path. Use keyword `default` to refer to a \ scheme default path:\n- m/44'/60'/0'/0/0 for the transparent \ @@ -8198,6 +8218,7 @@ pub mod args { pub output: Option, pub validator_alias: Option, pub use_device: bool, + pub device_transport: DeviceTransport, } impl Args for SignGenesisTxs { @@ -8206,11 +8227,13 @@ pub mod args { let output = OUTPUT.parse(matches); let validator_alias = ALIAS_OPT.parse(matches); let use_device = USE_DEVICE.parse(matches); + let device_transport = DEVICE_TRANSPORT.parse(matches); Self { path, output, validator_alias, use_device, + device_transport, } } @@ -8233,6 +8256,10 @@ pub mod args { "Derive an address and public key from the seed stored on the \ connected hardware wallet." ))) + .arg(DEVICE_TRANSPORT.def().help(wrap!( + "Select transport for hardware wallet from \"hid\" (default) \ + or \"tcp\"." + ))) } } diff --git a/crates/apps_lib/src/config/genesis/transactions.rs b/crates/apps_lib/src/config/genesis/transactions.rs index f066b429a7..268e37dda8 100644 --- a/crates/apps_lib/src/config/genesis/transactions.rs +++ b/crates/apps_lib/src/config/genesis/transactions.rs @@ -15,7 +15,7 @@ use namada_macros::BorshDeserializer; use namada_migrations::*; use namada_sdk::account::AccountPublicKeysMap; use namada_sdk::address::{Address, EstablishedAddress}; -use namada_sdk::args::Tx as TxArgs; +use namada_sdk::args::{DeviceTransport, Tx as TxArgs}; use namada_sdk::chain::ChainId; use namada_sdk::collections::HashSet; use namada_sdk::dec::Dec; @@ -93,6 +93,7 @@ fn get_tx_args(use_device: bool) -> TxArgs { password: None, memo: None, use_device, + device_transport: DeviceTransport::default(), } } diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index fdabbb0ef8..3a90d7fbba 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -2282,6 +2282,33 @@ pub struct Tx { pub memo: Option, /// Use device to sign the transaction pub use_device: bool, + /// Hardware Wallet transport - HID (USB) or TCP + pub device_transport: DeviceTransport, +} + +/// Hardware Wallet transport - HID (USB) or TCP +#[derive(Debug, Clone, Copy, Default)] +pub enum DeviceTransport { + /// HID transport (USB connected hardware wallet) + #[default] + Hid, + /// TCP transport + Tcp, +} + +impl FromStr for DeviceTransport { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "hid" => Ok(Self::Hid), + "tcp" => Ok(Self::Tcp), + raw => Err(format!( + "Unexpected device transport \"{raw}\". Valid options are \ + \"hid\" or \"tcp\"." + )), + } + } } /// Builder functions for Tx @@ -2470,6 +2497,8 @@ pub struct KeyDerive { pub prompt_bip39_passphrase: bool, /// Use device to generate key and address pub use_device: bool, + /// Hardware Wallet transport - HID (USB) or TCP + pub device_transport: DeviceTransport, } /// Wallet list arguments diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 463a4fd153..be78d995db 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -51,7 +51,7 @@ pub use std::marker::Sync as MaybeSync; use std::path::PathBuf; use std::str::FromStr; -use args::{InputAmount, SdkTypes}; +use args::{DeviceTransport, InputAmount, SdkTypes}; use io::Io; use masp::{ShieldedContext, ShieldedUtils}; use namada_core::address::Address; @@ -168,6 +168,7 @@ pub trait Namada: Sized + MaybeSync + MaybeSend { password: None, memo: None, use_device: false, + device_transport: DeviceTransport::default(), } } @@ -739,6 +740,7 @@ where password: None, memo: None, use_device: false, + device_transport: DeviceTransport::default(), }, } } From 3d124f15fa89c7b91b76dea06ca319ae7a31fb3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 7 Aug 2024 17:31:16 +0100 Subject: [PATCH 2/5] pick a device transport selected from the cli arg --- Cargo.lock | 109 +++++++++++++++- Cargo.toml | 3 + crates/apps_lib/Cargo.toml | 2 + crates/apps_lib/src/cli.rs | 12 +- crates/apps_lib/src/cli/wallet.rs | 75 ++++++----- crates/apps_lib/src/client/tx.rs | 118 ++++++++++++++---- crates/apps_lib/src/client/utils.rs | 14 ++- .../apps_lib/src/config/genesis/templates.rs | 15 +++ .../src/config/genesis/transactions.rs | 59 ++++++--- crates/apps_lib/src/config/genesis/utils.rs | 14 +-- 10 files changed, 334 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2806421428..4f09b3c19b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,9 +309,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", @@ -1975,6 +1975,51 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "encdec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25de94e10baa85551f7c65730423239370ed5bed60bf8d2a9cbf2683327ba421" +dependencies = [ + "encdec-base 0.9.0", + "encdec-macros", +] + +[[package]] +name = "encdec-base" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8542ff2a35da7fc94ffcf280f35dc759219c4b48fa930e0a0f268220d7fb6a" +dependencies = [ + "byteorder", + "num-traits 0.2.17", +] + +[[package]] +name = "encdec-base" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516ae3c7d00515548bf26a6531883335ceac2e9cde4938e70feea7456569be09" +dependencies = [ + "byteorder", + "heapless", + "num-traits 0.2.17", + "thiserror", +] + +[[package]] +name = "encdec-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15497932aae6b53bf8548cc63c65929b4fab6be54e28709c80fc72f5707eeed" +dependencies = [ + "darling 0.14.4", + "encdec-base 0.8.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "encoding_rs" version = "0.8.33" @@ -2281,7 +2326,7 @@ dependencies = [ "ethabi", "generic-array", "k256", - "num_enum", + "num_enum 0.7.1", "once_cell", "open-fastrlp", "rand 0.8.5", @@ -4028,6 +4073,25 @@ dependencies = [ "snafu", ] +[[package]] +name = "ledger-lib" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/rust-ledger?rev=f96f4559b3237d09218f7583df01acf36034ea79#f96f4559b3237d09218f7583df01acf36034ea79" +dependencies = [ + "async-trait", + "displaydoc", + "encdec", + "futures", + "ledger-proto", + "once_cell", + "strum 0.24.1", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", + "uuid 1.8.0", +] + [[package]] name = "ledger-namada-rs" version = "0.0.1" @@ -4045,6 +4109,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ledger-proto" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/rust-ledger?rev=f96f4559b3237d09218f7583df01acf36034ea79#f96f4559b3237d09218f7583df01acf36034ea79" +dependencies = [ + "bitflags 2.5.0", + "displaydoc", + "encdec", + "num_enum 0.6.1", + "thiserror", +] + [[package]] name = "ledger-transport" version = "0.10.0" @@ -4517,7 +4593,9 @@ dependencies = [ "git2", "itertools 0.12.1", "lazy_static", + "ledger-lib", "ledger-namada-rs", + "ledger-transport", "ledger-transport-hid", "linkme", "masp_primitives", @@ -4616,7 +4694,7 @@ dependencies = [ "num-rational", "num-traits 0.2.17", "num256", - "num_enum", + "num_enum 0.7.1", "pretty_assertions", "primitive-types", "proptest", @@ -5644,13 +5722,33 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + [[package]] name = "num_enum" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.7.1", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", ] [[package]] @@ -8563,6 +8661,7 @@ dependencies = [ "serde", "serde_json", "sharded-slab", + "smallvec", "thread_local", "tracing", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 2f36fabe62..1d1b866e2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,7 +123,10 @@ jubjub = "0.10" k256 = { version = "0.13.0", default-features = false, features = ["ecdsa", "pkcs8", "precomputed-tables", "serde", "std"]} konst = { version = "0.3.8", default-features = false } lazy_static = "1.4.0" +# TODO: upstreamed in https://github.com/ledger-community/rust-ledger/pull/9 +ledger-lib = { git = "https://github.com/heliaxdev/rust-ledger", rev = "f96f4559b3237d09218f7583df01acf36034ea79", default-features = false, features = ["transport_tcp"] } ledger-namada-rs = { git = "https://github.com/Zondax/ledger-namada", tag = "v0.0.24" } +ledger-transport = "0.10.0" ledger-transport-hid = "0.10.0" libc = "0.2.97" libloading = "0.7.2" diff --git a/crates/apps_lib/Cargo.toml b/crates/apps_lib/Cargo.toml index d443a6d682..3e4fe6d28d 100644 --- a/crates/apps_lib/Cargo.toml +++ b/crates/apps_lib/Cargo.toml @@ -56,7 +56,9 @@ futures.workspace = true itertools.workspace = true lazy_static = { workspace = true, optional = true } linkme = { workspace = true, optional = true } +ledger-lib = { workspace = true } ledger-namada-rs.workspace = true +ledger-transport.workspace = true ledger-transport-hid.workspace = true masp_primitives = { workspace = true, features = ["transparent-inputs"] } prost.workspace = true diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index 552fc4ef45..00ef7f19a6 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -3469,8 +3469,16 @@ pub mod args { pub const WITH_INDEXER: ArgOpt = arg_opt("with-indexer"); pub const TX_PATH: Arg = arg("tx-path"); pub const TX_PATH_OPT: ArgOpt = TX_PATH.opt(); - pub const DEVICE_TRANSPORT: ArgDefault = - arg_default("device-transport", DefaultFn(DeviceTransport::default)); + pub const DEVICE_TRANSPORT: ArgDefault = arg_default( + "device-transport", + DefaultFn(|| { + if let Ok(val) = std::env::var(DEVICE_TRANSPORT_ENV_VAR) { + return DeviceTransport::from_str(&val).unwrap(); + } + DeviceTransport::default() + }), + ); + pub const DEVICE_TRANSPORT_ENV_VAR: &str = "NAMADA_DEVICE_TRANSPORT"; /// Global command arguments #[derive(Clone, Debug)] diff --git a/crates/apps_lib/src/cli/wallet.rs b/crates/apps_lib/src/cli/wallet.rs index 546b3a8dac..1777cd5470 100644 --- a/crates/apps_lib/src/cli/wallet.rs +++ b/crates/apps_lib/src/cli/wallet.rs @@ -13,6 +13,7 @@ use ledger_transport_hid::hidapi::HidApi; use ledger_transport_hid::TransportNativeHID; use masp_primitives::zip32::ExtendedFullViewingKey; use namada_sdk::address::{Address, DecodeError}; +use namada_sdk::args::DeviceTransport; use namada_sdk::io::Io; use namada_sdk::key::*; use namada_sdk::masp::{ @@ -28,6 +29,7 @@ use crate::cli; use crate::cli::api::CliApi; use crate::cli::args::CliToSdk; use crate::cli::{args, cmds, Context}; +use crate::client::tx::NamadaAppTcpTransport; use crate::client::utils::PRE_GENESIS_DIR; use crate::tendermint_node::validator_key_to_json; use crate::wallet::{ @@ -446,7 +448,8 @@ async fn transparent_key_and_address_derive( allow_non_compliant, prompt_bip39_passphrase, use_device, - .. + shielded: _, + device_transport, }: args::KeyDerive, ) { let mut wallet = load_wallet(ctx); @@ -485,33 +488,49 @@ async fn transparent_key_and_address_derive( }) .0 } else { - let hidapi = HidApi::new().unwrap_or_else(|err| { - edisplay_line!(io, "Failed to create HidApi: {}", err); - cli::safe_exit(1) - }); - let app = NamadaApp::new( - TransportNativeHID::new(&hidapi).unwrap_or_else(|err| { - edisplay_line!(io, "Unable to connect to Ledger: {}", err); - cli::safe_exit(1) - }), - ); - let response = app - .get_address_and_pubkey( - &BIP44Path { - path: derivation_path.to_string(), - }, - true, - ) - .await - .unwrap_or_else(|err| { - edisplay_line!( - io, - "Unable to connect to query address and public key from \ - Ledger: {}", - err - ); - cli::safe_exit(1) - }); + macro_rules! get_address_and_pubkey { + ($app:expr) => { + $app.get_address_and_pubkey( + &BIP44Path { + path: derivation_path.to_string(), + }, + true, + ) + .await + .unwrap_or_else(|err| { + edisplay_line!( + io, + "Unable to connect to query address and public key \ + from Ledger (HID): {}", + err + ); + cli::safe_exit(1) + }) + }; + } + let response = match device_transport { + DeviceTransport::Hid => { + let hidapi = HidApi::new().unwrap_or_else(|err| { + edisplay_line!(io, "Failed to create HidApi: {}", err); + cli::safe_exit(1) + }); + let transport = TransportNativeHID::new(&hidapi) + .unwrap_or_else(|err| { + edisplay_line!( + io, + "Unable to connect to Ledger: {}", + err + ); + cli::safe_exit(1) + }); + let app = NamadaApp::new(transport); + get_address_and_pubkey!(app) + } + DeviceTransport::Tcp => { + let app = NamadaApp::new(NamadaAppTcpTransport); + get_address_and_pubkey!(app) + } + }; let pubkey = common::PublicKey::try_from_slice(&response.public_key) .expect("unable to decode public key from hardware wallet"); diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 835ff33c93..255bf7d989 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1,13 +1,19 @@ use std::fs::File; use std::io::Write; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::ops::Deref; +use std::str::FromStr; use borsh::BorshDeserialize; use borsh_ext::BorshSerializeExt; +use ledger_lib::transport::TcpInfo; +use ledger_lib::Transport; use ledger_namada_rs::{BIP44Path, NamadaApp}; +use ledger_transport::{APDUAnswer, APDUCommand}; use ledger_transport_hid::hidapi::HidApi; use ledger_transport_hid::TransportNativeHID; use namada_sdk::address::{Address, ImplicitAddress}; -use namada_sdk::args::TxBecomeValidator; +use namada_sdk::args::{DeviceTransport, TxBecomeValidator}; use namada_sdk::collections::HashSet; use namada_sdk::governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, @@ -65,12 +71,17 @@ pub async fn aux_signing_data( Ok(signing_data) } -pub async fn with_hardware_wallet<'a, U: WalletIo + Clone>( +pub async fn with_hardware_wallet<'a, U, T>( mut tx: Tx, pubkey: common::PublicKey, parts: HashSet, - (wallet, app): (&RwLock>, &NamadaApp), -) -> Result { + (wallet, app): (&RwLock>, &NamadaApp), +) -> Result +where + U: WalletIo + Clone, + T: ledger_transport::Exchange + Send + Sync, + ::Error: std::error::Error, +{ // Obtain derivation path let path = wallet .read() @@ -148,6 +159,41 @@ pub async fn with_hardware_wallet<'a, U: WalletIo + Clone>( Ok(tx) } +#[derive(Default)] +pub struct NamadaAppTcpTransport; + +#[ledger_transport::async_trait] +impl ledger_transport::Exchange for NamadaAppTcpTransport { + type AnswerType = Vec; + type Error = ledger_lib::Error; + + async fn exchange( + &self, + command: &APDUCommand, + ) -> Result, Self::Error> + where + I: Deref + Send + Sync, + { + use ledger_lib::Exchange; + let mut transport = ledger_lib::transport::TcpTransport::default(); + let ip = std::env::var("LEDGER_PROXY_ADDRESS") + .map(|s| Ipv4Addr::from_str(&s).unwrap()) + .unwrap_or(Ipv4Addr::LOCALHOST); + let port = std::env::var("LEDGER_PROXY_PORT") + .map(|s| u16::from_str(&s).unwrap()) + .unwrap_or(9999); + let mut device = transport + .connect(TcpInfo { + addr: SocketAddr::V4(SocketAddrV4::new(ip, port)), + }) + .await?; + let res = device + .exchange(&command.serialize(), std::time::Duration::from_secs(60)) + .await?; + Ok(APDUAnswer::from_answer(res).unwrap()) + } +} + // Sign the given transaction using a hardware wallet as a backup pub async fn sign( context: &N, @@ -157,29 +203,47 @@ pub async fn sign( ) -> Result<(), error::Error> { // Setup a reusable context for signing transactions using the Ledger if args.use_device { - // Setup a reusable context for signing transactions using the Ledger - let hidapi = HidApi::new().map_err(|err| { - error::Error::Other(format!("Failed to create Hidapi: {}", err)) - })?; - let app = NamadaApp::new(TransportNativeHID::new(&hidapi).map_err( - |err| { - error::Error::Other(format!( - "Unable to connect to Ledger: {}", - err - )) - }, - )?); - let with_hw_data = (context.wallet_lock(), &app); - // Finally, begin the signing with the Ledger as backup - context - .sign( - tx, - args, - signing_data, - with_hardware_wallet::, - with_hw_data, - ) - .await?; + macro_rules! sign { + ($app:expr) => { + let with_hw_data = (context.wallet_lock(), &$app); + // Finally, begin the signing with the Ledger as backup + context + .sign( + tx, + args, + signing_data, + with_hardware_wallet::, + with_hw_data, + ) + .await?; + }; + } + match args.device_transport { + DeviceTransport::Hid => { + tracing::info!("Signing over HID"); + let hidapi = HidApi::new().map_err(|err| { + error::Error::Other(format!( + "Failed to create Hidapi: {}", + err + )) + })?; + + let transport = + TransportNativeHID::new(&hidapi).map_err(|err| { + error::Error::Other(format!( + "Unable to connect to Ledger: {}", + err + )) + })?; + let app = NamadaApp::new(transport); + sign!(app); + } + DeviceTransport::Tcp => { + tracing::info!("Signing over TCP"); + let app = NamadaApp::new(NamadaAppTcpTransport); + sign!(app); + } + } } else { // Otherwise sign without a backup procedure context diff --git a/crates/apps_lib/src/client/utils.rs b/crates/apps_lib/src/client/utils.rs index 0d88cd54d5..eddbfed824 100644 --- a/crates/apps_lib/src/client/utils.rs +++ b/crates/apps_lib/src/client/utils.rs @@ -9,6 +9,7 @@ use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; use itertools::Either; +use namada_sdk::args::DeviceTransport; use namada_sdk::chain::ChainId; use namada_sdk::dec::Dec; use namada_sdk::key::*; @@ -865,6 +866,7 @@ async fn append_signature_to_signed_toml( input_txs: &Path, wallet: &RwLock>, use_device: bool, + device_transport: DeviceTransport, ) -> genesis::transactions::Transactions { // Parse signed txs toml to append new signatures to let mut genesis_txs = genesis::templates::read_transactions(input_txs) @@ -885,6 +887,7 @@ async fn append_signature_to_signed_toml( wallet, &genesis_txs.established_account, use_device, + device_transport, ) .await, ); @@ -904,6 +907,7 @@ async fn append_signature_to_signed_toml( validator account txs", ), use_device, + device_transport, ) .await, ); @@ -921,6 +925,7 @@ pub async fn sign_genesis_tx( output, validator_alias, use_device, + device_transport, }: args::SignGenesisTxs, ) { let (wallet, _wallet_file) = @@ -947,6 +952,7 @@ pub async fn sign_genesis_tx( &wallet_lock, maybe_pre_genesis_wallet.as_ref(), use_device, + device_transport, ) .await; if let Some(output_path) = output.as_ref() { @@ -964,7 +970,13 @@ pub async fn sign_genesis_tx( // In case we fail to parse unsigned txs, we will attempt to // parse signed txs and append new signatures to the existing // toml file - append_signature_to_signed_toml(&path, &wallet_lock, use_device).await + append_signature_to_signed_toml( + &path, + &wallet_lock, + use_device, + device_transport, + ) + .await }; match output { Some(output_path) => { diff --git a/crates/apps_lib/src/config/genesis/templates.rs b/crates/apps_lib/src/config/genesis/templates.rs index dd4592ed62..5c6e682a24 100644 --- a/crates/apps_lib/src/config/genesis/templates.rs +++ b/crates/apps_lib/src/config/genesis/templates.rs @@ -1026,6 +1026,21 @@ mod tests { ); } + /// Validate the `genesis/hardware` genesis templates. + #[test] + fn test_validate_hardware_genesis_templates() { + let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("genesis/hardware"); + assert!( + load_and_validate(&templates_dir).is_some(), + "Hardware genesis templates must be valid" + ); + } + #[test] fn test_read_balances() { let test_dir = tempdir().unwrap(); diff --git a/crates/apps_lib/src/config/genesis/transactions.rs b/crates/apps_lib/src/config/genesis/transactions.rs index 268e37dda8..e9cc107143 100644 --- a/crates/apps_lib/src/config/genesis/transactions.rs +++ b/crates/apps_lib/src/config/genesis/transactions.rs @@ -40,6 +40,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use super::templates::{DenominatedBalances, Parameters, ValidityPredicates}; +use crate::client::tx::NamadaAppTcpTransport; use crate::config::genesis::chain::DeriveEstablishedAddress; use crate::config::genesis::templates::{ TemplateValidation, Unvalidated, Validated, @@ -166,6 +167,7 @@ pub async fn sign_txs( wallet: &RwLock>, validator_wallet: Option<&ValidatorWallet>, use_device: bool, + device_transport: DeviceTransport, ) -> Transactions { let UnsignedTransactions { established_account, @@ -183,6 +185,7 @@ pub async fn sign_txs( wallet, &established_account, use_device, + device_transport, ) .await, ); @@ -209,6 +212,7 @@ pub async fn sign_txs( validator account txs", ), use_device, + device_transport, ) .await, ); @@ -355,6 +359,7 @@ pub async fn sign_validator_account_tx( wallet: &RwLock>, established_accounts: &[EstablishedAccountTx], use_device: bool, + device_transport: DeviceTransport, ) -> SignedValidatorAccountTx { let mut to_sign = match to_sign { Either::Right(signed_tx) => signed_tx, @@ -435,7 +440,9 @@ pub async fn sign_validator_account_tx( } }; - to_sign.sign(established_accounts, wallet, use_device).await; + to_sign + .sign(established_accounts, wallet, use_device, device_transport) + .await; to_sign } @@ -444,11 +451,14 @@ pub async fn sign_delegation_bond_tx( wallet: &RwLock>, established_accounts: &Option>, use_device: bool, + device_transport: DeviceTransport, ) -> SignedBondTx { let default = vec![]; let established_accounts = established_accounts.as_ref().unwrap_or(&default); - to_sign.sign(established_accounts, wallet, use_device).await; + to_sign + .sign(established_accounts, wallet, use_device, device_transport) + .await; to_sign } @@ -742,6 +752,7 @@ impl Signed { established_accounts: &[EstablishedAccountTx], wallet_lock: &RwLock>, use_device: bool, + device_transport: DeviceTransport, ) where T: BorshSerialize + TxToSign, { @@ -757,22 +768,36 @@ impl Signed { let mut tx = self.data.tx_to_sign(); - if use_device { - let hidapi = HidApi::new().expect("Failed to create Hidapi"); - let transport = TransportNativeHID::new(&hidapi) - .expect("Failed to create hardware wallet connection"); - let app = NamadaApp::new(transport); + macro_rules! sign_tx { + ($app:expr) => { + sign_tx( + wallet_lock, + &get_tx_args(use_device), + &mut tx, + signing_data, + utils::with_hardware_wallet, + (wallet_lock, &$app), + ) + .await + .expect("Failed to sign pre-genesis transaction.") + }; + } - sign_tx( - wallet_lock, - &get_tx_args(use_device), - &mut tx, - signing_data, - utils::with_hardware_wallet, - (wallet_lock, &app), - ) - .await - .expect("Failed to sign pre-genesis transaction."); + if use_device { + match device_transport { + DeviceTransport::Hid => { + let hidapi = + HidApi::new().expect("Failed to create Hidapi"); + let transport = TransportNativeHID::new(&hidapi) + .expect("Failed to create hardware wallet connection"); + let app = NamadaApp::new(transport); + sign_tx!(app); + } + DeviceTransport::Tcp => { + let app = NamadaApp::new(NamadaAppTcpTransport); + sign_tx!(app); + } + } } else { async fn software_wallet_sign( tx: Tx, diff --git a/crates/apps_lib/src/config/genesis/utils.rs b/crates/apps_lib/src/config/genesis/utils.rs index ee8d6bcadd..c5971d9d2c 100644 --- a/crates/apps_lib/src/config/genesis/utils.rs +++ b/crates/apps_lib/src/config/genesis/utils.rs @@ -2,7 +2,6 @@ use std::path::Path; use eyre::Context; use ledger_namada_rs::NamadaApp; -use ledger_transport_hid::TransportNativeHID; use namada_sdk::collections::HashSet; use namada_sdk::key::common; use namada_sdk::tx::Tx; @@ -50,15 +49,16 @@ pub fn write_toml( }) } -pub(super) async fn with_hardware_wallet<'a>( +pub(super) async fn with_hardware_wallet<'a, T>( tx: Tx, pubkey: common::PublicKey, parts: HashSet, - (wallet, app): ( - &RwLock>, - &NamadaApp, - ), -) -> Result { + (wallet, app): (&RwLock>, &NamadaApp), +) -> Result +where + T: ledger_transport::Exchange + Send + Sync, + ::Error: std::error::Error, +{ if parts.contains(&signing::Signable::FeeHeader) { Ok(tx) } else { From debc3586c08815cd51b3b706e47ed4ed8ad4bf37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 8 Aug 2024 11:51:33 +0100 Subject: [PATCH 3/5] genesis/hardware: update readme --- genesis/hardware/README.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/genesis/hardware/README.md b/genesis/hardware/README.md index 6b5b7e6d29..7abc5d42db 100644 --- a/genesis/hardware/README.md +++ b/genesis/hardware/README.md @@ -5,16 +5,30 @@ This directory contains genesis templates for a local network with a single vali If you're modifying any of the files here, you can run this to ensure that the changes are valid: ```shell -cargo watch -x "test test_validate_localnet_genesis_templates" +cargo watch -x "test test_validate_hardware_genesis_templates" ``` + ## Reproducibility -To ensure that the key generated by the hardware wallet for a given derivation path is always the same, the hardware wallet must be configured to use a known test mnemonic. Specifically, these genesis templates were generated using the following test mnemonic: `equip will roof matter pink blind book anxiety banner elbow sun young`. Instructions on how to configure the hardware wallet with this mnemonic can be found in the `Set a test mnemonic` section of https://github.com/Zondax/ledger-namada?tab=readme-ov-file#how-to-prepare-your-development-device . + +To ensure that the key generated by the hardware wallet for a given derivation path is always the same, the hardware wallet must be configured to use a known test mnemonic. Specifically, these genesis templates were generated using the following test mnemonic: `equip will roof matter pink blind book anxiety banner elbow sun young`. Instructions on how to configure the hardware wallet with this mnemonic can be found in the `Set a test mnemonic` section of . ## E2E and Integration Tests The E2E and integration tests can be made to use these hardware wallet based genesis templates by setting the environment variable `NAMADA_E2E_USE_DEVICE` to `true` when running `make test ...`. In order to ensure the success of these tests, please ensure that the following requirements are met: * The hardware wallet is connected and the Ledger app is open for the duration of the E2E tests * Transactions are promptly manually approved on the hardware wallet before the E2E test timeouts ellapse * The hardware wallet is configured with the same test mnemonic used to generate these genesis templates. See [reproducibility](#reproducibility). + +### Using Ledger emulator Speculos + +1. Build an elf file of the Namada ledger app +2. Build the emulator following the instructions from +3. Start the emulator with: + ```sh + ./speculos.py ../ledger-namada/app/output/app_s2.elf --seed "equip will roof matter pink blind book anxiety banner elbow sun young" + ``` +4. Set env var `export NAMADA_DEVICE_TRANSPORT=tcp` to connect to the emulator. The default ip/port is `127.0.0.1:9999`. To use a different ip/port set `LEDGER_PROXY_ADDRESS` and/or `LEDGER_PROXY_PORT`, respectively. + ## pre-genesis/wallet.toml + The pre-genesis balances wallet is located at [pre-genesis/wallet.toml](pre-genesis/wallet.toml) and can be re-generated from the repo's root dir with: ```shell @@ -96,7 +110,7 @@ The non-validator transactions are manually appended together in [src/pre-genesi ```shell cargo run --bin namadac -- --base-dir "genesis/localnet/src" utils \ - sign-genesis-txs \ + sign-genesis-txs --use-device \ --path "genesis/localnet/src/pre-genesis/unsigned-transactions.toml" \ --output "genesis/localnet/src/pre-genesis/signed-transactions.toml" ``` @@ -105,7 +119,7 @@ The validator transactions are signed using (note the extra `--alias` argument n ```shell cargo run --bin namadac -- --base-dir "genesis/localnet/src" utils \ - sign-genesis-txs \ + sign-genesis-txs --use-device \ --path "genesis/localnet/src/pre-genesis/validator-0/unsigned-transactions.toml" \ --output "genesis/localnet/src/pre-genesis/validator-0/signed-transactions.toml" \ --alias validator-0 @@ -131,4 +145,4 @@ cargo run --bin namadac -- --base-dir "genesis/localnet/src" utils \ ## Validation -A unit test `test_validate_localnet_genesis_templates` is setup to check validity of the localnet setup. +A unit test `test_validate_hardware_genesis_templates` is setup to check validity of the localnet setup. From 633d12dea5337458a807920d01aee30ea4a79fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 8 Aug 2024 12:56:54 +0100 Subject: [PATCH 4/5] changelog: add #3593 --- .changelog/unreleased/features/3593-ledger-tcp-transport.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/3593-ledger-tcp-transport.md diff --git a/.changelog/unreleased/features/3593-ledger-tcp-transport.md b/.changelog/unreleased/features/3593-ledger-tcp-transport.md new file mode 100644 index 0000000000..7c2d824a63 --- /dev/null +++ b/.changelog/unreleased/features/3593-ledger-tcp-transport.md @@ -0,0 +1,2 @@ +- Added support for Ledger wallet TCP transport. + ([\#3593](https://github.com/anoma/namada/pull/3593)) \ No newline at end of file From 5a1a69b7348e09e8e66043839eac92555e671ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 12 Aug 2024 12:00:16 +0100 Subject: [PATCH 5/5] refactor hardware wallet transport --- crates/apps_lib/src/cli/wallet.rs | 68 ++++-------- crates/apps_lib/src/client/tx.rs | 103 +++--------------- .../src/config/genesis/transactions.rs | 46 +++----- crates/apps_lib/src/wallet/mod.rs | 2 + crates/apps_lib/src/wallet/transport.rs | 95 ++++++++++++++++ 5 files changed, 147 insertions(+), 167 deletions(-) create mode 100644 crates/apps_lib/src/wallet/transport.rs diff --git a/crates/apps_lib/src/cli/wallet.rs b/crates/apps_lib/src/cli/wallet.rs index 1777cd5470..b5f61578b1 100644 --- a/crates/apps_lib/src/cli/wallet.rs +++ b/crates/apps_lib/src/cli/wallet.rs @@ -9,11 +9,8 @@ use borsh_ext::BorshSerializeExt; use color_eyre::eyre::Result; use itertools::sorted; use ledger_namada_rs::{BIP44Path, NamadaApp}; -use ledger_transport_hid::hidapi::HidApi; -use ledger_transport_hid::TransportNativeHID; use masp_primitives::zip32::ExtendedFullViewingKey; use namada_sdk::address::{Address, DecodeError}; -use namada_sdk::args::DeviceTransport; use namada_sdk::io::Io; use namada_sdk::key::*; use namada_sdk::masp::{ @@ -29,11 +26,10 @@ use crate::cli; use crate::cli::api::CliApi; use crate::cli::args::CliToSdk; use crate::cli::{args, cmds, Context}; -use crate::client::tx::NamadaAppTcpTransport; use crate::client::utils::PRE_GENESIS_DIR; use crate::tendermint_node::validator_key_to_json; use crate::wallet::{ - self, read_and_confirm_encryption_password, CliWalletUtils, + self, read_and_confirm_encryption_password, CliWalletUtils, WalletTransport, }; impl CliApi { @@ -488,49 +484,25 @@ async fn transparent_key_and_address_derive( }) .0 } else { - macro_rules! get_address_and_pubkey { - ($app:expr) => { - $app.get_address_and_pubkey( - &BIP44Path { - path: derivation_path.to_string(), - }, - true, - ) - .await - .unwrap_or_else(|err| { - edisplay_line!( - io, - "Unable to connect to query address and public key \ - from Ledger (HID): {}", - err - ); - cli::safe_exit(1) - }) - }; - } - let response = match device_transport { - DeviceTransport::Hid => { - let hidapi = HidApi::new().unwrap_or_else(|err| { - edisplay_line!(io, "Failed to create HidApi: {}", err); - cli::safe_exit(1) - }); - let transport = TransportNativeHID::new(&hidapi) - .unwrap_or_else(|err| { - edisplay_line!( - io, - "Unable to connect to Ledger: {}", - err - ); - cli::safe_exit(1) - }); - let app = NamadaApp::new(transport); - get_address_and_pubkey!(app) - } - DeviceTransport::Tcp => { - let app = NamadaApp::new(NamadaAppTcpTransport); - get_address_and_pubkey!(app) - } - }; + let transport = WalletTransport::from_arg(device_transport); + let app = NamadaApp::new(transport); + let response = app + .get_address_and_pubkey( + &BIP44Path { + path: derivation_path.to_string(), + }, + true, + ) + .await + .unwrap_or_else(|err| { + edisplay_line!( + io, + "Unable to connect to query address and public key from \ + Ledger: {}", + err + ); + cli::safe_exit(1) + }); let pubkey = common::PublicKey::try_from_slice(&response.public_key) .expect("unable to decode public key from hardware wallet"); diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 255bf7d989..96430cdf94 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1,19 +1,11 @@ use std::fs::File; use std::io::Write; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; -use std::ops::Deref; -use std::str::FromStr; use borsh::BorshDeserialize; use borsh_ext::BorshSerializeExt; -use ledger_lib::transport::TcpInfo; -use ledger_lib::Transport; use ledger_namada_rs::{BIP44Path, NamadaApp}; -use ledger_transport::{APDUAnswer, APDUCommand}; -use ledger_transport_hid::hidapi::HidApi; -use ledger_transport_hid::TransportNativeHID; use namada_sdk::address::{Address, ImplicitAddress}; -use namada_sdk::args::{DeviceTransport, TxBecomeValidator}; +use namada_sdk::args::TxBecomeValidator; use namada_sdk::collections::HashSet; use namada_sdk::governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, @@ -38,7 +30,9 @@ use crate::client::tx::tx::ProcessTxResponse; use crate::config::TendermintMode; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::tendermint_node; -use crate::wallet::{gen_validator_keys, read_and_confirm_encryption_password}; +use crate::wallet::{ + gen_validator_keys, read_and_confirm_encryption_password, WalletTransport, +}; /// Wrapper around `signing::aux_signing_data` that stores the optional /// disposable address to the wallet @@ -159,41 +153,6 @@ where Ok(tx) } -#[derive(Default)] -pub struct NamadaAppTcpTransport; - -#[ledger_transport::async_trait] -impl ledger_transport::Exchange for NamadaAppTcpTransport { - type AnswerType = Vec; - type Error = ledger_lib::Error; - - async fn exchange( - &self, - command: &APDUCommand, - ) -> Result, Self::Error> - where - I: Deref + Send + Sync, - { - use ledger_lib::Exchange; - let mut transport = ledger_lib::transport::TcpTransport::default(); - let ip = std::env::var("LEDGER_PROXY_ADDRESS") - .map(|s| Ipv4Addr::from_str(&s).unwrap()) - .unwrap_or(Ipv4Addr::LOCALHOST); - let port = std::env::var("LEDGER_PROXY_PORT") - .map(|s| u16::from_str(&s).unwrap()) - .unwrap_or(9999); - let mut device = transport - .connect(TcpInfo { - addr: SocketAddr::V4(SocketAddrV4::new(ip, port)), - }) - .await?; - let res = device - .exchange(&command.serialize(), std::time::Duration::from_secs(60)) - .await?; - Ok(APDUAnswer::from_answer(res).unwrap()) - } -} - // Sign the given transaction using a hardware wallet as a backup pub async fn sign( context: &N, @@ -203,47 +162,19 @@ pub async fn sign( ) -> Result<(), error::Error> { // Setup a reusable context for signing transactions using the Ledger if args.use_device { - macro_rules! sign { - ($app:expr) => { - let with_hw_data = (context.wallet_lock(), &$app); - // Finally, begin the signing with the Ledger as backup - context - .sign( - tx, - args, - signing_data, - with_hardware_wallet::, - with_hw_data, - ) - .await?; - }; - } - match args.device_transport { - DeviceTransport::Hid => { - tracing::info!("Signing over HID"); - let hidapi = HidApi::new().map_err(|err| { - error::Error::Other(format!( - "Failed to create Hidapi: {}", - err - )) - })?; - - let transport = - TransportNativeHID::new(&hidapi).map_err(|err| { - error::Error::Other(format!( - "Unable to connect to Ledger: {}", - err - )) - })?; - let app = NamadaApp::new(transport); - sign!(app); - } - DeviceTransport::Tcp => { - tracing::info!("Signing over TCP"); - let app = NamadaApp::new(NamadaAppTcpTransport); - sign!(app); - } - } + let transport = WalletTransport::from_arg(args.device_transport); + let app = NamadaApp::new(transport); + let with_hw_data = (context.wallet_lock(), &app); + // Finally, begin the signing with the Ledger as backup + context + .sign( + tx, + args, + signing_data, + with_hardware_wallet::, + with_hw_data, + ) + .await?; } else { // Otherwise sign without a backup procedure context diff --git a/crates/apps_lib/src/config/genesis/transactions.rs b/crates/apps_lib/src/config/genesis/transactions.rs index e9cc107143..34d086f30b 100644 --- a/crates/apps_lib/src/config/genesis/transactions.rs +++ b/crates/apps_lib/src/config/genesis/transactions.rs @@ -8,8 +8,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use borsh_ext::BorshSerializeExt; use itertools::{Either, Itertools}; use ledger_namada_rs::NamadaApp; -use ledger_transport_hid::hidapi::HidApi; -use ledger_transport_hid::TransportNativeHID; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; @@ -40,13 +38,12 @@ use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use super::templates::{DenominatedBalances, Parameters, ValidityPredicates}; -use crate::client::tx::NamadaAppTcpTransport; use crate::config::genesis::chain::DeriveEstablishedAddress; use crate::config::genesis::templates::{ TemplateValidation, Unvalidated, Validated, }; use crate::config::genesis::{utils, GenesisAddress}; -use crate::wallet::CliWalletUtils; +use crate::wallet::{CliWalletUtils, WalletTransport}; /// Dummy chain id used to sign [`Tx`] objects at pre-genesis. const NAMADA_GENESIS_TX_CHAIN_ID: &str = "namada-genesis"; @@ -768,36 +765,19 @@ impl Signed { let mut tx = self.data.tx_to_sign(); - macro_rules! sign_tx { - ($app:expr) => { - sign_tx( - wallet_lock, - &get_tx_args(use_device), - &mut tx, - signing_data, - utils::with_hardware_wallet, - (wallet_lock, &$app), - ) - .await - .expect("Failed to sign pre-genesis transaction.") - }; - } - if use_device { - match device_transport { - DeviceTransport::Hid => { - let hidapi = - HidApi::new().expect("Failed to create Hidapi"); - let transport = TransportNativeHID::new(&hidapi) - .expect("Failed to create hardware wallet connection"); - let app = NamadaApp::new(transport); - sign_tx!(app); - } - DeviceTransport::Tcp => { - let app = NamadaApp::new(NamadaAppTcpTransport); - sign_tx!(app); - } - } + let transport = WalletTransport::from_arg(device_transport); + let app = NamadaApp::new(transport); + sign_tx( + wallet_lock, + &get_tx_args(use_device), + &mut tx, + signing_data, + utils::with_hardware_wallet, + (wallet_lock, &app), + ) + .await + .expect("Failed to sign pre-genesis transaction.") } else { async fn software_wallet_sign( tx: Tx, diff --git a/crates/apps_lib/src/wallet/mod.rs b/crates/apps_lib/src/wallet/mod.rs index c95bdebf0d..b841240c59 100644 --- a/crates/apps_lib/src/wallet/mod.rs +++ b/crates/apps_lib/src/wallet/mod.rs @@ -1,6 +1,7 @@ pub mod defaults; pub mod pre_genesis; mod store; +mod transport; use std::io::{self, Write}; use std::path::{Path, PathBuf}; @@ -17,6 +18,7 @@ use namada_sdk::wallet::{ pub use namada_sdk::wallet::{ValidatorData, ValidatorKeys}; use rand_core::OsRng; pub use store::wallet_file; +pub use transport::{TransportTcp, WalletTransport}; use zeroize::Zeroizing; use crate::cli; diff --git a/crates/apps_lib/src/wallet/transport.rs b/crates/apps_lib/src/wallet/transport.rs new file mode 100644 index 0000000000..f8353de362 --- /dev/null +++ b/crates/apps_lib/src/wallet/transport.rs @@ -0,0 +1,95 @@ +//! Hardware wallet transport over HID or TCP + +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::ops::Deref; +use std::str::FromStr; + +use ledger_lib::transport::TcpInfo; +use ledger_lib::Transport; +use ledger_transport::{APDUAnswer, APDUCommand}; +use ledger_transport_hid::hidapi::HidApi; +use ledger_transport_hid::TransportNativeHID; +use namada_sdk::args; + +/// Hardware wallet transport +pub enum WalletTransport { + /// HID transport + HID(TransportNativeHID), + /// TCP transport + TCP(TransportTcp), +} + +impl WalletTransport { + pub fn from_arg(arg: args::DeviceTransport) -> Self { + match arg { + args::DeviceTransport::Hid => { + let hidapi = HidApi::new() + .expect("Must be able to instantiate a hidapi context"); + let transport = TransportNativeHID::new(&hidapi) + .expect("Must be able to connect to a HID wallet"); + Self::HID(transport) + } + args::DeviceTransport::Tcp => Self::TCP(TransportTcp), + } + } +} + +#[ledger_transport::async_trait] +impl ledger_transport::Exchange for WalletTransport { + type AnswerType = Vec; + type Error = std::io::Error; + + async fn exchange( + &self, + command: &APDUCommand, + ) -> Result, Self::Error> + where + I: Deref + Send + Sync, + { + match self { + WalletTransport::HID(transport) => transport + .exchange(command) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), + WalletTransport::TCP(transport) => transport + .exchange(command) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), + } + } +} + +/// Hardware wallet TCP transport +#[derive(Default)] +pub struct TransportTcp; + +#[ledger_transport::async_trait] +impl ledger_transport::Exchange for TransportTcp { + type AnswerType = Vec; + type Error = ledger_lib::Error; + + async fn exchange( + &self, + command: &APDUCommand, + ) -> Result, Self::Error> + where + I: Deref + Send + Sync, + { + use ledger_lib::Exchange; + let mut transport = ledger_lib::transport::TcpTransport::default(); + let ip = std::env::var("LEDGER_PROXY_ADDRESS") + .map(|s| Ipv4Addr::from_str(&s).unwrap()) + .unwrap_or(Ipv4Addr::LOCALHOST); + let port = std::env::var("LEDGER_PROXY_PORT") + .map(|s| u16::from_str(&s).unwrap()) + .unwrap_or(9999); + let mut device = transport + .connect(TcpInfo { + addr: SocketAddr::V4(SocketAddrV4::new(ip, port)), + }) + .await?; + let res = device + .exchange(&command.serialize(), std::time::Duration::from_secs(60)) + .await?; + Ok(APDUAnswer::from_answer(res).unwrap()) + } +}