From 37f302d67d162e1316e02d5da3e1c3aee3755e53 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 19 Nov 2024 15:37:36 -0500 Subject: [PATCH] fix: add first proper test using ledger and CLI --- Cargo.lock | 3 + Cargo.toml | 3 + cmd/crates/soroban-test/Cargo.toml | 4 + cmd/crates/soroban-test/src/lib.rs | 12 +++ .../soroban-test/tests/it/integration.rs | 3 + .../tests/it/integration/emulator.rs | 83 +++++++++++++++++++ .../soroban-test/tests/it/integration/tx.rs | 13 ++- cmd/crates/stellar-ledger/Cargo.toml | 2 +- .../emulator_test_support/http_transport.rs | 2 +- .../src/emulator_test_support/speculos.rs | 58 +++++++++++-- .../src/emulator_test_support/util.rs | 9 +- .../tests/test/emulator_tests.rs | 62 +++++++------- cmd/soroban-cli/Cargo.toml | 1 + cmd/soroban-cli/src/config/address.rs | 4 +- cmd/soroban-cli/src/config/secret.rs | 6 +- cmd/soroban-cli/src/config/sign_with.rs | 9 +- cmd/soroban-cli/src/signer.rs | 43 +++++++++- 17 files changed, 261 insertions(+), 56 deletions(-) create mode 100644 cmd/crates/soroban-test/tests/it/integration/emulator.rs diff --git a/Cargo.lock b/Cargo.lock index 6e71d2a9a..df4d0ea3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4610,8 +4610,11 @@ dependencies = [ "soroban-ledger-snapshot", "soroban-spec", "soroban-spec-tools", + "stellar-ledger", "stellar-rpc-client", "stellar-strkey 0.0.11", + "test-case", + "testcontainers", "thiserror", "tokio", "toml", diff --git a/Cargo.toml b/Cargo.toml index 73dbd4cb5..33f8363fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,7 +108,10 @@ walkdir = "2.5.0" toml_edit = "0.22.20" toml = "0.8.19" reqwest = "0.12.7" +# testing predicates = "3.1.2" +testcontainers = { version = "0.20.1" } + [profile.test-wasms] inherits = "release" diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index f694ec533..86656ceb2 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -24,6 +24,7 @@ stellar-strkey = { workspace = true } sep5 = { workspace = true } soroban-cli = { workspace = true } soroban-rpc = { workspace = true } +stellar-ledger = { workspace = true } thiserror = "1.0.31" sha2 = "0.10.6" @@ -32,6 +33,7 @@ assert_fs = "1.0.7" predicates = { workspace = true } fs_extra = "1.3.0" toml = { workspace = true } +testcontainers = { workspace = true } [dev-dependencies] @@ -42,6 +44,8 @@ walkdir = "2.4.0" ulid.workspace = true ed25519-dalek = { workspace = true } hex = { workspace = true } +test-case = "3.3.1" [features] it = [] +emulator-tests = ["stellar-ledger/emulator-tests"] diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 82e05b78e..6645acf61 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -36,6 +36,10 @@ use soroban_cli::{ }; mod wasm; +use stellar_ledger::emulator_test_support::{ + enable_hash_signing, get_container, speculos::Speculos, wait_for_emulator_start_text, +}; +use testcontainers::ContainerAsync; pub use wasm::Wasm; pub const TEST_ACCOUNT: &str = "test"; @@ -308,6 +312,14 @@ impl TestEnv { pub fn client(&self) -> soroban_rpc::Client { soroban_rpc::Client::new(&self.rpc_url).unwrap() } + + pub async fn speculos_container(ledger_device_model: &str) -> ContainerAsync { + let container = get_container(ledger_device_model).await; + let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); + wait_for_emulator_start_text(ui_host_port).await; + enable_hash_signing(ui_host_port).await; + container + } } pub fn temp_ledger_file() -> OsString { diff --git a/cmd/crates/soroban-test/tests/it/integration.rs b/cmd/crates/soroban-test/tests/it/integration.rs index 3ec0d61ed..a8a637b20 100644 --- a/cmd/crates/soroban-test/tests/it/integration.rs +++ b/cmd/crates/soroban-test/tests/it/integration.rs @@ -9,3 +9,6 @@ mod snapshot; mod tx; mod util; mod wrap; + +#[cfg(feature = "emulator-tests")] +mod emulator; diff --git a/cmd/crates/soroban-test/tests/it/integration/emulator.rs b/cmd/crates/soroban-test/tests/it/integration/emulator.rs new file mode 100644 index 000000000..9c4ccd1c0 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration/emulator.rs @@ -0,0 +1,83 @@ +use stellar_ledger::{Blob, Error}; + +use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; +use std::sync::Arc; + +use soroban_cli::xdr::{ + self, Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber, + Transaction, TransactionExt, Uint256, +}; + +use stellar_ledger::emulator_test_support::*; + +use test_case::test_case; + +use crate::integration::util::{deploy_contract, DeployKind, HELLO_WORLD}; + +// #[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +// #[test_case("nanosp"; "when the device is NanoS Plus")] +#[tokio::test] +async fn test_get_public_key(ledger_device_model: &str) { + let sandbox = Arc::new(TestEnv::new()); + let container = TestEnv::speculos_container(ledger_device_model).await; + let host_port = container.get_host_port_ipv4(9998).await.unwrap(); + let ui_host_port = container.get_host_port_ipv4(5000).await.unwrap(); + + let ledger = ledger(host_port).await; + + let key = ledger.get_public_key(&0.into()).await.unwrap(); + let account = &key.to_string(); + sandbox.fund_account(account); + sandbox + .new_assert_cmd("contract") + .arg("install") + .args([ + "--wasm", + HELLO_WORLD.path().as_os_str().to_str().unwrap(), + "--source", + account, + ]) + .assert() + .success(); + + let tx_simulated = + deploy_contract(&sandbox, HELLO_WORLD, DeployKind::SimOnly, Some(account)).await; + dbg!("{tx_simulated}"); + let key = ledger.get_public_key(&0.into()).await.unwrap(); + println!("{key}"); + let sign = tokio::task::spawn_blocking({ + let sandbox = Arc::clone(&sandbox); + + move || { + sandbox + .new_assert_cmd("tx") + .arg("sign") + .arg("--sign-with-ledger") + .write_stdin(tx_simulated.as_bytes()) + .env("SPECULOS_PORT", host_port.to_string()) + .env("RUST_LOGS", "trace") + .assert() + .success() + .stdout_as_str() + } + }); + let approve = tokio::task::spawn(approve_tx_hash_signature( + ui_host_port, + ledger_device_model.to_string(), + )); + + let response = sign.await.unwrap(); + approve.await.unwrap(); + + dbg!("{tx_signed}"); + + sandbox + .clone() + .new_assert_cmd("tx") + .arg("send") + .write_stdin(response.as_bytes()) + .assert() + .success() + .stdout(predicates::str::contains("SUCCESS")); +} diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index c3cd2693b..00d907e77 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -56,10 +56,19 @@ async fn txn_hash() { #[tokio::test] async fn build_simulate_sign_send() { let sandbox = &TestEnv::new(); + build_sim_sign_send(sandbox, "test", "--sign-with-key=test").await; +} + +pub(crate) async fn build_sim_sign_send(sandbox: &TestEnv, account: &str, sign_with: &str) { sandbox .new_assert_cmd("contract") .arg("install") - .args(["--wasm", HELLO_WORLD.path().as_os_str().to_str().unwrap()]) + .args([ + "--wasm", + HELLO_WORLD.path().as_os_str().to_str().unwrap(), + "--source", + account, + ]) .assert() .success(); @@ -69,7 +78,7 @@ async fn build_simulate_sign_send() { let tx_signed = sandbox .new_assert_cmd("tx") .arg("sign") - .arg("--sign-with-key=test") + .arg(sign_with) .write_stdin(tx_simulated.as_bytes()) .assert() .success() diff --git a/cmd/crates/stellar-ledger/Cargo.toml b/cmd/crates/stellar-ledger/Cargo.toml index 17335a453..8b2d22b41 100644 --- a/cmd/crates/stellar-ledger/Cargo.toml +++ b/cmd/crates/stellar-ledger/Cargo.toml @@ -35,7 +35,7 @@ tokio = { version = "1", features = ["full"] } reqwest = { workspace = true, features = ["json"] } phf = { version = "0.11.2", features = ["macros"], optional = true } async-trait = { workspace = true } -testcontainers = { version = "0.20.1", optional = true } +testcontainers = { workspace = true, optional = true } [dependencies.stellar-xdr] workspace = true diff --git a/cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs index c90c28d92..c82c46cac 100644 --- a/cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs @@ -72,7 +72,7 @@ impl Exchange for EmulatorHttpTransport { let resp: Response = HttpClient::new() .post(&self.url) .headers(headers) - .timeout(Duration::from_secs(25)) + .timeout(Duration::from_secs(60)) .json(&request) .send() .await diff --git a/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs index 9084b8c85..f8b95c13a 100644 --- a/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::HashMap, path::PathBuf}; +use std::{borrow::Cow, collections::HashMap, path::PathBuf, str::FromStr}; use testcontainers::{ core::{Mount, WaitFor}, Image, @@ -55,14 +55,54 @@ impl Speculos { } fn get_cmd(ledger_device_model: String) -> String { - let device_model = ledger_device_model.clone(); - let container_elf_path = match device_model.as_str() { - "nanos" => format!("{DEFAULT_APP_PATH}/stellarNanoSApp.elf"), - "nanosp" => format!("{DEFAULT_APP_PATH}/stellarNanoSPApp.elf"), - "nanox" => format!("{DEFAULT_APP_PATH}/stellarNanoXApp.elf"), - _ => panic!("Unsupported device model"), - }; - format!("/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN --display headless -s {TEST_SEED_PHRASE} -m {device_model} {container_elf_path}") + let device_model: DeviceModel = ledger_device_model.parse().unwrap(); + let container_elf_path = format!("{DEFAULT_APP_PATH}/{}", device_model.as_file()); + format!( + "/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN \ + --display headless \ + -s {TEST_SEED_PHRASE} \ + -m {device_model} {container_elf_path}" + ) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum DeviceModel { + NanoS, + NanoSP, + NanoX, +} + +impl FromStr for DeviceModel { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "nanos" => Ok(DeviceModel::NanoS), + "nanosp" => Ok(DeviceModel::NanoSP), + "nanox" => Ok(DeviceModel::NanoX), + _ => Err(format!("Unsupported device model: {}", s)), + } + } +} + +impl std::fmt::Display for DeviceModel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + DeviceModel::NanoS => write!(f, "nanos"), + DeviceModel::NanoSP => write!(f, "nanosp"), + DeviceModel::NanoX => write!(f, "nanox"), + } + } +} + +impl DeviceModel { + pub fn as_file(&self) -> &str { + match self { + DeviceModel::NanoS => "stellarNanoSApp.elf", + DeviceModel::NanoSP => "stellarNanoSPApp.elf", + DeviceModel::NanoX => "stellarNanoXApp.elf", + } } } diff --git a/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs index cc442eca9..50479d532 100644 --- a/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs @@ -1,4 +1,3 @@ -use ledger_transport::Exchange; use serde::Deserialize; use std::ops::Range; use std::sync::LazyLock; @@ -24,7 +23,7 @@ pub fn test_network_hash() -> Hash { Hash(sha2::Sha256::digest(TEST_NETWORK_PASSPHRASE).into()) } -pub async fn ledger(host_port: u16) -> LedgerSigner { +pub async fn ledger(host_port: u16) -> LedgerSigner { LedgerSigner::new(get_http_transport("127.0.0.1", host_port).await.unwrap()) } @@ -83,9 +82,9 @@ struct EventsResponse { events: Vec, } -pub async fn get_container(ledger_device_model: String) -> ContainerAsync { +pub async fn get_container(ledger_device_model: &str) -> ContainerAsync { let (tcp_port_1, tcp_port_2) = get_available_ports(2); - Speculos::new(ledger_device_model) + Speculos::new(ledger_device_model.to_string()) .with_mapped_port(tcp_port_1, ContainerPort::Tcp(9998)) .with_mapped_port(tcp_port_2, ContainerPort::Tcp(5000)) .start() @@ -110,7 +109,7 @@ pub fn get_available_ports(n: usize) -> (u16, u16) { (ports[0], ports[1]) } -pub async fn get_http_transport(host: &str, port: u16) -> Result { +pub async fn get_http_transport(host: &str, port: u16) -> Result { let max_retries = 5; let mut retries = 0; let mut wait_time = Duration::from_secs(1); diff --git a/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs b/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs index c01f55849..880ac95ff 100644 --- a/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs +++ b/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs @@ -12,12 +12,12 @@ use stellar_ledger::emulator_test_support::*; use test_case::test_case; -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_get_public_key(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_get_public_key(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -39,12 +39,12 @@ async fn test_get_public_key(ledger_device_model: String) { } } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_get_app_configuration(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_get_app_configuration(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -62,12 +62,12 @@ async fn test_get_app_configuration(ledger_device_model: String) { }; } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -112,7 +112,7 @@ async fn test_sign_tx(ledger_device_model: String) { fee: 100, seq_num: SequenceNumber(1), cond: Preconditions::None, - memo: Memo::Text("Stellar".as_bytes().try_into().unwrap()), + memo: Memo::Text("Stellar".try_into().unwrap()), ext: TransactionExt::V0, operations: [Operation { source_account: Some(MuxedAccount::Ed25519(Uint256(source_account_bytes))), @@ -130,7 +130,10 @@ async fn test_sign_tx(ledger_device_model: String) { let ledger = Arc::clone(&ledger); async move { ledger.sign_transaction(path, tx, test_network_hash()).await } }); - let approve = tokio::task::spawn(approve_tx_signature(ui_host_port, ledger_device_model)); + let approve = tokio::task::spawn(approve_tx_signature( + ui_host_port, + ledger_device_model.to_string(), + )); let result = sign.await.unwrap(); let _ = approve.await.unwrap(); @@ -146,12 +149,12 @@ async fn test_sign_tx(ledger_device_model: String) { }; } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -170,12 +173,12 @@ async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: } } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); @@ -201,7 +204,10 @@ async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: Str let ledger = Arc::clone(&ledger); async move { ledger.sign_transaction_hash(path, &test_hash).await } }); - let approve = tokio::task::spawn(approve_tx_hash_signature(ui_host_port, ledger_device_model)); + let approve = tokio::task::spawn(approve_tx_hash_signature( + ui_host_port, + ledger_device_model.to_string(), + )); let response = sign.await.unwrap(); let _ = approve.await.unwrap(); diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 3e65072b5..234a70da9 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -37,6 +37,7 @@ doctest = false [features] default = [] opt = ["dep:wasm-opt"] +emulator-tests = ["stellar-ledger/emulator-tests"] [dependencies] stellar-xdr = { workspace = true, features = ["cli"] } diff --git a/cmd/soroban-cli/src/config/address.rs b/cmd/soroban-cli/src/config/address.rs index d7c1ebaba..43a9521ec 100644 --- a/cmd/soroban-cli/src/config/address.rs +++ b/cmd/soroban-cli/src/config/address.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use crate::{ - signer::{self, native_ledger}, + signer::{self, ledger}, xdr, }; @@ -80,7 +80,7 @@ impl Address { )) }), Address::Ledger(hd_path) => Ok(xdr::MuxedAccount::Ed25519( - native_ledger(*hd_path)?.public_key().await?.0.into(), + ledger(*hd_path).await?.public_key().await?.0.into(), )), } } diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index 6e5703d57..1c8f89c5c 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -6,7 +6,7 @@ use stellar_strkey::ed25519::{PrivateKey, PublicKey}; use crate::{ print::Print, - signer::{self, native_ledger, LocalKey, Signer, SignerKind}, + signer::{self, ledger, LocalKey, Signer, SignerKind}, utils, }; @@ -133,7 +133,7 @@ impl Secret { )?) } - pub fn signer(&self, index: Option, print: Print) -> Result { + pub async fn signer(&self, index: Option, print: Print) -> Result { let kind = match self { Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => { let key = self.key_pair(index)?; @@ -144,7 +144,7 @@ impl Secret { .unwrap_or_default() .try_into() .expect("uszie bigger than u32"); - SignerKind::Ledger(native_ledger(hd_path)?) + SignerKind::Ledger(ledger(hd_path).await?) } }; Ok(Signer { kind, print }) diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index a747a5524..8446f086e 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -1,6 +1,6 @@ use crate::{ print::Print, - signer::{self, native_ledger, Signer, SignerKind}, + signer::{self, ledger, Signer, SignerKind}, xdr::{self, TransactionEnvelope}, }; use clap::arg; @@ -72,12 +72,13 @@ impl Args { print, } } else if self.sign_with_ledger { - let ledger = native_ledger( + let ledger = ledger( self.hd_path .unwrap_or_default() .try_into() .unwrap_or_default(), - )?; + ) + .await?; Signer { kind: SignerKind::Ledger(ledger), print, @@ -85,7 +86,7 @@ impl Args { } else { let key_or_name = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; let secret = locator.key(key_or_name)?; - secret.signer(self.hd_path, print)? + secret.signer(self.hd_path, print).await? }; Ok(signer.sign_tx_env(tx, network).await?) } diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index 1593d8c9c..3ef79ecaf 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -209,7 +209,10 @@ pub struct Signer { #[allow(clippy::module_name_repetitions, clippy::large_enum_variant)] pub enum SignerKind { Local(LocalKey), + #[cfg(not(feature = "emulator-tests"))] Ledger(Ledger), + #[cfg(feature = "emulator-tests")] + Ledger(Ledger), Lab, } @@ -279,12 +282,29 @@ impl Ledger { Ok(DecoratedSignature { hint, signature }) } + pub async fn sign_transaction( + &self, + tx: Transaction, + network_passphrase: &str, + ) -> Result { + let network_id = Hash(Sha256::digest(network_passphrase).into()); + let signature = self + .signer + .sign_transaction(self.index, tx, network_id) + .await?; + let key = self.public_key().await?; + let hint = SignatureHint(key.0[28..].try_into()?); + let signature = Signature(signature.try_into()?); + Ok(DecoratedSignature { hint, signature }) + } + pub async fn public_key(&self) -> Result { Ok(self.signer.get_public_key(&self.index.into()).await?) } } -pub fn native_ledger(hd_path: u32) -> Result, Error> { +#[cfg(not(feature = "emulator-tests"))] +pub async fn ledger(hd_path: u32) -> Result, Error> { let signer = stellar_ledger::native()?; Ok(Ledger { index: hd_path, @@ -292,6 +312,27 @@ pub fn native_ledger(hd_path: u32) -> Result, Error> }) } +#[cfg(feature = "emulator-tests")] +pub async fn ledger( + hd_path: u32, +) -> Result< + Ledger, + Error, +> { + use stellar_ledger::emulator_test_support::ledger as emulator_ledger; + // port from SPECULOS_PORT ENV var + let host_port: u16 = std::env::var("SPECULOS_PORT") + .expect("SPECULOS_PORT env var not set") + .parse() + .expect("port must be a number"); + let signer = emulator_ledger(host_port).await; + + Ok(Ledger { + index: hd_path, + signer, + }) +} + impl LocalKey { pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?);