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 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 b5f7eddf00..19bbbd3897 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -3468,6 +3468,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(|| { + 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)] @@ -7033,6 +7043,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, }) } } @@ -7163,6 +7174,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() @@ -7204,6 +7224,7 @@ pub mod args { None => TxExpiration::Default, } }; + let device_transport = DEVICE_TRANSPORT.parse(matches); Self { dry_run, dry_run_wrapper, @@ -7227,6 +7248,7 @@ pub mod args { output_folder, memo, use_device, + device_transport, } } } @@ -7356,22 +7378,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, } } @@ -7401,6 +7425,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 \ @@ -8191,6 +8219,7 @@ pub mod args { pub output: Option, pub validator_alias: Option, pub use_device: bool, + pub device_transport: DeviceTransport, } impl Args for SignGenesisTxs { @@ -8199,11 +8228,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, } } @@ -8226,6 +8257,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/cli/wallet.rs b/crates/apps_lib/src/cli/wallet.rs index 546b3a8dac..b5f61578b1 100644 --- a/crates/apps_lib/src/cli/wallet.rs +++ b/crates/apps_lib/src/cli/wallet.rs @@ -9,8 +9,6 @@ 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::io::Io; @@ -31,7 +29,7 @@ use crate::cli::{args, cmds, Context}; 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 { @@ -446,7 +444,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,16 +484,8 @@ 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 transport = WalletTransport::from_arg(device_transport); + let app = NamadaApp::new(transport); let response = app .get_address_and_pubkey( &BIP44Path { diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 481774ebd3..c640aec100 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -4,8 +4,6 @@ use std::io::Write; use borsh::BorshDeserialize; use borsh_ext::BorshSerializeExt; use ledger_namada_rs::{BIP44Path, NamadaApp}; -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::collections::HashSet; @@ -32,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 @@ -66,12 +66,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() @@ -158,18 +163,8 @@ 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 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 @@ -177,7 +172,7 @@ pub async fn sign( tx, args, signing_data, - with_hardware_wallet::, + with_hardware_wallet::, with_hw_data, ) .await?; diff --git a/crates/apps_lib/src/client/utils.rs b/crates/apps_lib/src/client/utils.rs index 2776493700..dc643d6a43 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::*; @@ -863,6 +864,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) @@ -883,6 +885,7 @@ async fn append_signature_to_signed_toml( wallet, &genesis_txs.established_account, use_device, + device_transport, ) .await, ); @@ -902,6 +905,7 @@ async fn append_signature_to_signed_toml( validator account txs", ), use_device, + device_transport, ) .await, ); @@ -919,6 +923,7 @@ pub async fn sign_genesis_tx( output, validator_alias, use_device, + device_transport, }: args::SignGenesisTxs, ) { let (wallet, _wallet_file) = @@ -945,6 +950,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() { @@ -962,7 +968,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 f066b429a7..34d086f30b 100644 --- a/crates/apps_lib/src/config/genesis/transactions.rs +++ b/crates/apps_lib/src/config/genesis/transactions.rs @@ -8,14 +8,12 @@ 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::*; 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; @@ -45,7 +43,7 @@ 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"; @@ -93,6 +91,7 @@ fn get_tx_args(use_device: bool) -> TxArgs { password: None, memo: None, use_device, + device_transport: DeviceTransport::default(), } } @@ -165,6 +164,7 @@ pub async fn sign_txs( wallet: &RwLock>, validator_wallet: Option<&ValidatorWallet>, use_device: bool, + device_transport: DeviceTransport, ) -> Transactions { let UnsignedTransactions { established_account, @@ -182,6 +182,7 @@ pub async fn sign_txs( wallet, &established_account, use_device, + device_transport, ) .await, ); @@ -208,6 +209,7 @@ pub async fn sign_txs( validator account txs", ), use_device, + device_transport, ) .await, ); @@ -354,6 +356,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, @@ -434,7 +437,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 } @@ -443,11 +448,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 } @@ -741,6 +749,7 @@ impl Signed { established_accounts: &[EstablishedAccountTx], wallet_lock: &RwLock>, use_device: bool, + device_transport: DeviceTransport, ) where T: BorshSerialize + TxToSign, { @@ -757,11 +766,8 @@ 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 transport = WalletTransport::from_arg(device_transport); let app = NamadaApp::new(transport); - sign_tx( wallet_lock, &get_tx_args(use_device), @@ -771,7 +777,7 @@ impl Signed { (wallet_lock, &app), ) .await - .expect("Failed to sign pre-genesis transaction."); + .expect("Failed to sign pre-genesis transaction.") } 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 { 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()) + } +} 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 f8ee403a88..884883fda5 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(), }, } } 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.