From 4d47962ec5e462bea2c702e3f6f2b82296ebb369 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Thu, 7 May 2026 12:44:26 -0700 Subject: [PATCH 1/3] Sign Soroban auth entries from Ledger identities. --- .../src/commands/contract/arg_parsing.rs | 24 +++--- .../src/commands/contract/deploy/wasm.rs | 3 +- .../src/commands/contract/invoke.rs | 4 +- cmd/soroban-cli/src/commands/message/sign.rs | 26 +++---- cmd/soroban-cli/src/config/mod.rs | 10 +-- cmd/soroban-cli/src/config/secret.rs | 13 +++- cmd/soroban-cli/src/config/sign_with.rs | 21 ++--- cmd/soroban-cli/src/signer/ledger.rs | 78 +++++++++++++------ cmd/soroban-cli/src/signer/mod.rs | 51 ++++++++---- 9 files changed, 144 insertions(+), 86 deletions(-) diff --git a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs index 9120e03a2c..2d186c2604 100644 --- a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs +++ b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs @@ -93,25 +93,25 @@ fn running_cmd() -> String { format!("{} --", args.join(" ")) } -pub async fn build_host_function_parameters( +pub fn build_host_function_parameters( contract_id: &stellar_strkey::Contract, slop: &[OsString], spec_entries: &[ScSpecEntry], config: &config::Args, ) -> Result { - build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, true).await + build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, true) } -pub async fn build_constructor_parameters( +pub fn build_constructor_parameters( contract_id: &stellar_strkey::Contract, slop: &[OsString], spec_entries: &[ScSpecEntry], config: &config::Args, ) -> Result { - build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, false).await + build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, false) } -async fn build_host_function_parameters_with_filter( +fn build_host_function_parameters_with_filter( contract_id: &stellar_strkey::Contract, slop: &[OsString], spec_entries: &[ScSpecEntry], @@ -122,7 +122,7 @@ async fn build_host_function_parameters_with_filter( let cmd = build_clap_command(&spec, filter_constructor)?; let (function, matches_) = parse_command_matches(cmd, slop)?; let func = get_function_spec(&spec, &function)?; - let (parsed_args, signers) = parse_function_arguments(&func, &matches_, &spec, config).await?; + let (parsed_args, signers) = parse_function_arguments(&func, &matches_, &spec, config)?; let invoke_args = build_invoke_contract_args(contract_id, &function, parsed_args)?; Ok((function, spec, invoke_args, signers)) @@ -187,7 +187,7 @@ fn get_function_spec(spec: &Spec, function: &str) -> Result::new(); for i in func.inputs.iter() { - parse_single_argument(i, matches_, spec, config, &mut signers, &mut parsed_args).await?; + parse_single_argument(i, matches_, spec, config, &mut signers, &mut parsed_args)?; } Ok((parsed_args, signers)) } -async fn parse_single_argument( +fn parse_single_argument( input: &stellar_xdr::curr::ScSpecFunctionInputV0, matches_: &clap::ArgMatches, spec: &Spec, @@ -234,7 +234,7 @@ async fn parse_single_argument( ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress ) { let trimmed_s = s.trim_matches('"'); - if let Some(signer) = resolve_signer(trimmed_s, config).await { + if let Some(signer) = resolve_signer(trimmed_s, config) { signers.push(signer); } } @@ -464,10 +464,10 @@ fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result Option { +fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option { let secret = config.locator.get_secret_key(addr_or_alias).ok()?; let print = Print::new(false); - let signer = secret.signer(config.hd_path(), print).await.ok()?; + let signer = secret.signer(config.hd_path(), print).ok()?; Some(signer) } diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index dbfe34caaf..00e9b168d8 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -385,8 +385,7 @@ impl Cmd { &slop, &entries, config, - ) - .await? + )? .2, ) } diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index c5641dc88f..be9a2a8f17 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -270,7 +270,7 @@ impl Cmd { if let Some(spec_entries) = &spec_entries { // For testing wasm arg parsing - build_host_function_parameters(&contract_id, &self.slop, spec_entries, config).await?; + build_host_function_parameters(&contract_id, &self.slop, spec_entries, config)?; } let client = network.rpc_client()?; @@ -295,7 +295,7 @@ impl Cmd { .map_err(Error::from)?; let params = - build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config).await?; + build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config)?; let (function, spec, host_function_params, signers) = params; diff --git a/cmd/soroban-cli/src/commands/message/sign.rs b/cmd/soroban-cli/src/commands/message/sign.rs index 3a20e0dccb..a56c604a2d 100644 --- a/cmd/soroban-cli/src/commands/message/sign.rs +++ b/cmd/soroban-cli/src/commands/message/sign.rs @@ -85,11 +85,11 @@ impl Cmd { let secret = self .locator .get_secret_key_with_hd_path(key_or_name, self.hd_path)?; - let signer = secret.signer(self.hd_path, print.clone()).await?; + let signer = secret.signer(self.hd_path, print.clone())?; let public_key = signer.get_public_key()?; // Encode signature as base64 - let signature_base64 = sep_53_sign(&message_bytes, signer)?; + let signature_base64 = sep_53_sign(&message_bytes, signer).await?; print.infoln(format!("Signer: {public_key}")); println!("{signature_base64}"); @@ -126,14 +126,14 @@ impl Cmd { /// Sign the given message bytes with the provided signer, returning the base64-encoded signature. /// /// Expects the message bytes to be the raw message (without SEP-53 prefix). -fn sep_53_sign(message_bytes: &[u8], signer: Signer) -> Result { +async fn sep_53_sign(message_bytes: &[u8], signer: Signer) -> Result { // Create SEP-53 payload let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); payload.extend_from_slice(SEP53_PREFIX.as_bytes()); payload.extend_from_slice(message_bytes); let hash: [u8; 32] = Sha256::digest(&payload).into(); - let signature = signer.sign_payload(hash)?; + let signature = signer.sign_payload(hash).await?; Ok(BASE64.encode(signature.to_bytes())) } @@ -177,8 +177,8 @@ mod tests { } } - #[test] - fn test_sign_simple() { + #[tokio::test] + async fn test_sign_simple() { // SEP-53 - test case 1 let message = "Hello, World!".to_string(); let expected_signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; @@ -194,13 +194,13 @@ mod tests { let signer = build_signer_for_test_key(); let message_bytes = cmd.get_message_bytes().unwrap(); - let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap(); + let signature_base64 = sep_53_sign(&message_bytes, signer).await.unwrap(); assert_eq!(signature_base64, expected_signature); } - #[test] - fn test_sign_japanese() { + #[tokio::test] + async fn test_sign_japanese() { // SEP-53 - test case 2 let message = "こんにちは、世界!".to_string(); let expected_signature = "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA=="; @@ -216,13 +216,13 @@ mod tests { let signer = build_signer_for_test_key(); let message_bytes = cmd.get_message_bytes().unwrap(); - let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap(); + let signature_base64 = sep_53_sign(&message_bytes, signer).await.unwrap(); assert_eq!(signature_base64, expected_signature); } - #[test] - fn test_sign_base64() { + #[tokio::test] + async fn test_sign_base64() { // SEP-53 - test case 3 let message = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=".to_string(); let expected_signature = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ=="; @@ -238,7 +238,7 @@ mod tests { let signer = build_signer_for_test_key(); let message_bytes = cmd.get_message_bytes().unwrap(); - let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap(); + let signature_base64 = sep_53_sign(&message_bytes, signer).await.unwrap(); assert_eq!(signature_base64, expected_signature); } diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index b24e9f855a..17743eb2cf 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -154,12 +154,10 @@ impl Args { let client = network.rpc_client()?; let latest_ledger = client.get_latest_ledger().await?.sequence; let seq_num = latest_ledger + 60; // ~ 5 min - Ok(signer::sign_soroban_authorizations( - tx, - signers, - seq_num, - &network.network_passphrase, - )?) + Ok( + signer::sign_soroban_authorizations(tx, signers, seq_num, &network.network_passphrase) + .await?, + ) } pub fn get_network(&self) -> Result { diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index 12d942508e..d7402db73b 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -6,7 +6,9 @@ use stellar_strkey::ed25519::{PrivateKey, PublicKey}; use crate::{ print::Print, - signer::{self, ledger, secure_store, LocalKey, SecureStoreEntry, Signer, SignerKind}, + signer::{ + self, ledger::LedgerEntry, secure_store, LocalKey, SecureStoreEntry, Signer, SignerKind, + }, utils, }; @@ -207,7 +209,7 @@ impl Secret { } } - pub async fn signer(&self, hd_path: Option, print: Print) -> Result { + pub fn signer(&self, hd_path: Option, print: Print) -> Result { let kind = match self { Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => { let key = self.key_pair(hd_path)?; @@ -215,14 +217,17 @@ impl Secret { } Secret::Ledger { hardware: HardwareKind::Ledger, + public_key, hd_path: cached_hd_path, - .. } => { let effective = hd_path.or(*cached_hd_path).unwrap_or_default(); let hd_path: u32 = effective .try_into() .map_err(|_| Error::HdPathOutOfRange(effective))?; - SignerKind::Ledger(ledger::new(hd_path).await?) + SignerKind::Ledger(LedgerEntry { + hd_path, + public_key: Some(PublicKey::from_string(public_key)?), + }) } Secret::SecureStore { entry_name, diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index b026653fd6..1d21bd6dc0 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -1,7 +1,7 @@ use crate::{ config::UnresolvedMuxedAccount, print::Print, - signer::{self, ledger, Signer, SignerKind}, + signer::{self, ledger::LedgerEntry, Signer, SignerKind}, xdr::{self, TransactionEnvelope}, }; @@ -88,15 +88,16 @@ impl Args { print, } } else if self.sign_with_ledger { - let ledger = ledger::new( - self.hd_path - .unwrap_or_default() - .try_into() - .unwrap_or_default(), - ) - .await?; + let hd_path = self + .hd_path + .unwrap_or_default() + .try_into() + .unwrap_or_default(); Signer { - kind: SignerKind::Ledger(ledger), + kind: SignerKind::Ledger(LedgerEntry { + hd_path, + public_key: None, + }), print, } } else { @@ -110,7 +111,7 @@ impl Args { }; let secret = locator.get_secret_key_with_hd_path(key_or_name, self.hd_path)?; - secret.signer(self.hd_path, print).await? + secret.signer(self.hd_path, print)? }; Ok(signer.sign_tx_env(tx, network).await?) } diff --git a/cmd/soroban-cli/src/signer/ledger.rs b/cmd/soroban-cli/src/signer/ledger.rs index 534cebec06..9577169686 100644 --- a/cmd/soroban-cli/src/signer/ledger.rs +++ b/cmd/soroban-cli/src/signer/ledger.rs @@ -22,6 +22,7 @@ pub enum Error { mod ledger_impl { use super::Error; use crate::xdr::{DecoratedSignature, Hash, Signature, SignatureHint, Transaction}; + use ed25519_dalek::Signature as Ed25519Signature; use sha2::{Digest, Sha256}; use stellar_ledger::{Blob as _, Exchange, LedgerSigner}; @@ -30,6 +31,42 @@ mod ledger_impl { #[cfg(feature = "emulator-tests")] pub type LedgerType = Ledger; + // Pure-data signer for Ledger identities. Mirrors `SecureStoreEntry`: + // holds no live transport, opens HID lazily inside each sign call so the + // device stays free between operations and can never collide with a + // concurrent transport elsewhere in the process. + pub struct LedgerEntry { + pub hd_path: u32, + pub public_key: Option, + } + + impl LedgerEntry { + pub async fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { + let live = new(self.hd_path).await?; + let key = match self.public_key { + Some(pk) => pk, + None => live.public_key().await?, + }; + let hint = SignatureHint(key.0[28..].try_into()?); + let signature = Signature( + live.signer + .sign_transaction_hash(live.index, &tx_hash) + .await? + .try_into()?, + ); + Ok(DecoratedSignature { hint, signature }) + } + + pub async fn sign_payload(&self, payload: [u8; 32]) -> Result { + let live = new(self.hd_path).await?; + let bytes = live + .signer + .sign_transaction_hash(live.index, &payload) + .await?; + Ok(Ed25519Signature::from_bytes(bytes.as_slice().try_into()?)) + } + } + pub struct Ledger { pub(crate) index: u32, pub(crate) signer: LedgerSigner, @@ -65,21 +102,6 @@ mod ledger_impl { } impl Ledger { - pub async fn sign_transaction_hash( - &self, - tx_hash: &[u8; 32], - ) -> Result { - let key = self.public_key().await?; - let hint = SignatureHint(key.0[28..].try_into()?); - let signature = Signature( - self.signer - .sign_transaction_hash(self.index, tx_hash) - .await? - .try_into()?, - ); - Ok(DecoratedSignature { hint, signature }) - } - pub async fn sign_transaction( &self, tx: Transaction, @@ -106,6 +128,7 @@ mod ledger_impl { mod ledger_impl { use super::Error; use crate::xdr::{DecoratedSignature, Transaction}; + use ed25519_dalek::Signature as Ed25519Signature; use std::marker::PhantomData; pub type LedgerType = Ledger; @@ -115,20 +138,29 @@ mod ledger_impl { _marker: PhantomData, } - #[allow(clippy::unused_async)] - pub async fn new(_hd_path: u32) -> Result, Error> { - Err(Error::FeatureNotEnabled) + pub struct LedgerEntry { + pub hd_path: u32, + pub public_key: Option, } - impl Ledger { + impl LedgerEntry { #[allow(clippy::unused_async)] - pub async fn sign_transaction_hash( - &self, - _tx_hash: &[u8; 32], - ) -> Result { + pub async fn sign_tx_hash(&self, _tx_hash: [u8; 32]) -> Result { Err(Error::FeatureNotEnabled) } + #[allow(clippy::unused_async)] + pub async fn sign_payload(&self, _payload: [u8; 32]) -> Result { + Err(Error::FeatureNotEnabled) + } + } + + #[allow(clippy::unused_async)] + pub async fn new(_hd_path: u32) -> Result, Error> { + Err(Error::FeatureNotEnabled) + } + + impl Ledger { #[allow(clippy::unused_async)] pub async fn sign_transaction( &self, diff --git a/cmd/soroban-cli/src/signer/mod.rs b/cmd/soroban-cli/src/signer/mod.rs index 4e1578b552..0bec9b3f07 100644 --- a/cmd/soroban-cli/src/signer/mod.rs +++ b/cmd/soroban-cli/src/signer/mod.rs @@ -1,4 +1,5 @@ use crate::{ + signer::ledger::LedgerEntry, utils::fee_bump_transaction_hash, xdr::{ self, AccountId, DecoratedSignature, FeeBumpTransactionEnvelope, Hash, HashIdPreimage, @@ -56,7 +57,7 @@ pub enum Error { /// /// If a SorobanAuthorizationEntry needs signing, but a signature cannot be produced for it, /// return an Error -pub fn sign_soroban_authorizations( +pub async fn sign_soroban_authorizations( raw: &Transaction, signers: &[Signer], signature_expiration_ledger: u32, @@ -119,7 +120,8 @@ pub fn sign_soroban_authorizations( signer, signature_expiration_ledger, &network_id, - )?; + ) + .await?; signed_auths.push(signed_entry); auths_modified = true; } @@ -151,7 +153,7 @@ pub fn sign_soroban_authorizations( Ok(Some(tx)) } -fn sign_soroban_authorization_entry( +async fn sign_soroban_authorization_entry( raw: &SorobanAuthorizationEntry, signer: &Signer, signature_expiration_ledger: u32, @@ -178,7 +180,7 @@ fn sign_soroban_authorization_entry( let payload = Sha256::digest(preimage); let p: [u8; 32] = payload.as_slice().try_into()?; - let signature = signer.sign_payload(p)?; + let signature = signer.sign_payload(p).await?; let public_key_vec = signer.get_public_key()?.0.to_vec(); let map = ScMap::sorted_from(vec![ @@ -214,7 +216,7 @@ pub struct Signer { #[allow(clippy::module_name_repetitions, clippy::large_enum_variant)] pub enum SignerKind { Local(LocalKey), - Ledger(ledger::LedgerType), + Ledger(LedgerEntry), Lab, SecureStore(SecureStoreEntry), } @@ -269,23 +271,23 @@ impl Signer { } } - // when we implement this for ledger we'll need it to be async so we can await for the ledger's public key pub fn get_public_key(&self) -> Result { match &self.kind { SignerKind::Local(local_key) => Ok(stellar_strkey::ed25519::PublicKey::from_payload( local_key.key.verifying_key().as_bytes(), )?), - SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"), + SignerKind::Ledger(ledger) => Ok(ledger + .public_key + .expect("Ledger signers reachable here are built from Secret::Ledger and always carry a cached public key")), SignerKind::Lab => Err(Error::ReturningSignatureFromLab), SignerKind::SecureStore(secure_store_entry) => secure_store_entry.get_public_key(), } } - // when we implement this for ledger we'll need it to be async so we can await the user approved the tx on the ledger device - pub fn sign_payload(&self, payload: [u8; 32]) -> Result { + pub async fn sign_payload(&self, payload: [u8; 32]) -> Result { match &self.kind { SignerKind::Local(local_key) => local_key.sign_payload(payload), - SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"), + SignerKind::Ledger(ledger) => Ok(ledger.sign_payload(payload).await?), SignerKind::Lab => Err(Error::ReturningSignatureFromLab), SignerKind::SecureStore(secure_store_entry) => secure_store_entry.sign_payload(payload), } @@ -300,10 +302,7 @@ impl Signer { match &self.kind { SignerKind::Local(key) => key.sign_tx_hash(tx_hash), SignerKind::Lab => Lab::sign_tx_env(tx_env, network, &self.print), - SignerKind::Ledger(ledger) => ledger - .sign_transaction_hash(&tx_hash) - .await - .map_err(Error::from), + SignerKind::Ledger(ledger) => ledger.sign_tx_hash(tx_hash).await.map_err(Error::from), SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash), } } @@ -379,3 +378,27 @@ impl SecureStoreEntry { Ok(sig) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::signer::ledger::LedgerEntry; + + const TEST_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ"; + + #[test] + fn ledger_signer_get_public_key_returns_cached_without_device() { + let pk = stellar_strkey::ed25519::PublicKey::from_string(TEST_PUBLIC_KEY).unwrap(); + let signer = Signer { + kind: SignerKind::Ledger(LedgerEntry { + hd_path: 0, + public_key: Some(pk), + }), + print: Print::new(true), + }; + assert_eq!( + signer.get_public_key().unwrap().to_string(), + TEST_PUBLIC_KEY + ); + } +} From 70dd1e0a781186c11ca80948ce6eb029970f40c4 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Thu, 7 May 2026 13:33:50 -0700 Subject: [PATCH 2/3] Reject --hd-path overrides on Ledger aliases. --- cmd/soroban-cli/src/commands/keys/add.rs | 16 ++--- cmd/soroban-cli/src/commands/keys/generate.rs | 2 +- .../src/commands/keys/public_key.rs | 12 ++-- cmd/soroban-cli/src/commands/keys/secret.rs | 2 +- .../commands/ledger/entry/fetch/account.rs | 2 +- .../ledger/entry/fetch/account_data.rs | 2 +- .../src/commands/ledger/entry/fetch/offer.rs | 2 +- .../commands/ledger/entry/fetch/trustline.rs | 2 +- cmd/soroban-cli/src/commands/message/sign.rs | 2 +- .../src/commands/message/verify.rs | 2 +- cmd/soroban-cli/src/config/address.rs | 2 +- cmd/soroban-cli/src/config/key.rs | 4 +- cmd/soroban-cli/src/config/locator.rs | 6 +- cmd/soroban-cli/src/config/mod.rs | 2 +- cmd/soroban-cli/src/config/sc_address.rs | 2 +- cmd/soroban-cli/src/config/secret.rs | 72 +++++++++---------- cmd/soroban-cli/src/config/sign_with.rs | 9 +-- cmd/soroban-cli/src/signer/keyring.rs | 8 +-- cmd/soroban-cli/src/signer/mod.rs | 2 +- cmd/soroban-cli/src/signer/secure_store.rs | 14 ++-- 20 files changed, 74 insertions(+), 91 deletions(-) diff --git a/cmd/soroban-cli/src/commands/keys/add.rs b/cmd/soroban-cli/src/commands/keys/add.rs index dd6b4c6f97..141ddabdb4 100644 --- a/cmd/soroban-cli/src/commands/keys/add.rs +++ b/cmd/soroban-cli/src/commands/keys/add.rs @@ -45,9 +45,6 @@ pub enum Error { #[error("--hd-path is not valid with a secret key; secret keys cannot be derived")] HdPathNotSupportedForSecretKey, - - #[error("--hd-path {0} is out of range for a Ledger account index")] - HdPathOutOfRange(usize), } #[derive(Debug, clap::Parser, Clone)] @@ -94,7 +91,7 @@ pub struct Cmd { /// without re-passing the flag. Not valid with `--public-key` or a raw /// secret key. #[arg(long)] - pub hd_path: Option, + pub hd_path: Option, } impl Cmd { @@ -125,9 +122,10 @@ impl Cmd { } async fn derive_ledger_secret(&self) -> Result { - let raw = self.hd_path.unwrap_or(0); - let index: u32 = raw.try_into().map_err(|_| Error::HdPathOutOfRange(raw))?; - let public_key = ledger::new(index).await?.public_key().await?; + let public_key = ledger::new(self.hd_path.unwrap_or_default()) + .await? + .public_key() + .await?; Ok(Secret::Ledger { hardware: HardwareKind::Ledger, public_key: public_key.to_string(), @@ -181,7 +179,7 @@ impl Cmd { } } -fn build_secret(input: &str, hd_path: Option) -> Result { +fn build_secret(input: &str, hd_path: Option) -> Result { let secret: Secret = input.parse()?; match (secret, hd_path) { (Secret::SecretKey { .. }, Some(_)) => Err(Error::HdPathNotSupportedForSecretKey), @@ -248,7 +246,7 @@ mod tests { fn cmd_with_public_key( public_key: &str, - hd_path: Option, + hd_path: Option, ) -> (tempfile::TempDir, locator::Args, Cmd) { let (temp_dir, locator, mut cmd) = set_up_test(); cmd.public_key = Some(public_key.to_string()); diff --git a/cmd/soroban-cli/src/commands/keys/generate.rs b/cmd/soroban-cli/src/commands/keys/generate.rs index 9ef33bacf4..391a175a7f 100644 --- a/cmd/soroban-cli/src/commands/keys/generate.rs +++ b/cmd/soroban-cli/src/commands/keys/generate.rs @@ -55,7 +55,7 @@ pub struct Cmd { /// `--secure-store` or plain seed-phrase storage it is persisted on the identity /// so later commands derive the same account without re-passing the flag. #[arg(long)] - pub hd_path: Option, + pub hd_path: Option, #[command(flatten)] pub network: network::Args, diff --git a/cmd/soroban-cli/src/commands/keys/public_key.rs b/cmd/soroban-cli/src/commands/keys/public_key.rs index 8fcfc96af0..a28ea1dac8 100644 --- a/cmd/soroban-cli/src/commands/keys/public_key.rs +++ b/cmd/soroban-cli/src/commands/keys/public_key.rs @@ -11,9 +11,6 @@ pub enum Error { #[error(transparent)] Ledger(#[from] ledger::Error), - - #[error("--hd-path {0} is out of range for a Ledger account index")] - HdPathOutOfRange(usize), } #[derive(Debug, clap::Parser, Clone)] @@ -26,7 +23,7 @@ pub struct Cmd { /// If identity is a seed phrase use this hd path, default is 0. /// With --ledger this is the Ledger account index (default 0). #[arg(long)] - pub hd_path: Option, + pub hd_path: Option, /// Derive the address from a connected Ledger hardware wallet at /// `m/44'/148'/N'`, where `N` defaults to 0 and can be set with @@ -46,9 +43,10 @@ impl Cmd { pub async fn public_key(&self) -> Result { if self.ledger { - let raw = self.hd_path.unwrap_or(0); - let index: u32 = raw.try_into().map_err(|_| Error::HdPathOutOfRange(raw))?; - return Ok(ledger::new(index).await?.public_key().await?); + return Ok(ledger::new(self.hd_path.unwrap_or_default()) + .await? + .public_key() + .await?); } let name = self .name diff --git a/cmd/soroban-cli/src/commands/keys/secret.rs b/cmd/soroban-cli/src/commands/keys/secret.rs index ceb79d886e..9fc00279c2 100644 --- a/cmd/soroban-cli/src/commands/keys/secret.rs +++ b/cmd/soroban-cli/src/commands/keys/secret.rs @@ -29,7 +29,7 @@ pub struct Cmd { /// If identity is a seed phrase use this hd path, default is 0 #[arg(long, conflicts_with = "phrase")] - pub hd_path: Option, + pub hd_path: Option, #[command(flatten)] pub locator: locator::Args, diff --git a/cmd/soroban-cli/src/commands/ledger/entry/fetch/account.rs b/cmd/soroban-cli/src/commands/ledger/entry/fetch/account.rs index 6908dafff0..4c8ba43e56 100644 --- a/cmd/soroban-cli/src/commands/ledger/entry/fetch/account.rs +++ b/cmd/soroban-cli/src/commands/ledger/entry/fetch/account.rs @@ -20,7 +20,7 @@ pub struct Cmd { /// If identity is a seed phrase use this hd path, default is 0 #[arg(long)] - pub hd_path: Option, + pub hd_path: Option, } #[derive(thiserror::Error, Debug)] diff --git a/cmd/soroban-cli/src/commands/ledger/entry/fetch/account_data.rs b/cmd/soroban-cli/src/commands/ledger/entry/fetch/account_data.rs index b2ac26c8b6..7de3b7b43f 100644 --- a/cmd/soroban-cli/src/commands/ledger/entry/fetch/account_data.rs +++ b/cmd/soroban-cli/src/commands/ledger/entry/fetch/account_data.rs @@ -24,7 +24,7 @@ pub struct Cmd { /// If identity is a seed phrase use this hd path, default is 0 #[arg(long)] - pub hd_path: Option, + pub hd_path: Option, } #[derive(thiserror::Error, Debug)] diff --git a/cmd/soroban-cli/src/commands/ledger/entry/fetch/offer.rs b/cmd/soroban-cli/src/commands/ledger/entry/fetch/offer.rs index 0f1f6bedd4..1dac4002d1 100644 --- a/cmd/soroban-cli/src/commands/ledger/entry/fetch/offer.rs +++ b/cmd/soroban-cli/src/commands/ledger/entry/fetch/offer.rs @@ -24,7 +24,7 @@ pub struct Cmd { /// If identity is a seed phrase use this hd path, default is 0 #[arg(long)] - pub hd_path: Option, + pub hd_path: Option, } #[derive(thiserror::Error, Debug)] diff --git a/cmd/soroban-cli/src/commands/ledger/entry/fetch/trustline.rs b/cmd/soroban-cli/src/commands/ledger/entry/fetch/trustline.rs index 9b5a844b6e..3869007a71 100644 --- a/cmd/soroban-cli/src/commands/ledger/entry/fetch/trustline.rs +++ b/cmd/soroban-cli/src/commands/ledger/entry/fetch/trustline.rs @@ -27,7 +27,7 @@ pub struct Cmd { /// If account is a seed phrase use this hd path, default is 0 #[arg(long)] - pub hd_path: Option, + pub hd_path: Option, } #[derive(thiserror::Error, Debug)] diff --git a/cmd/soroban-cli/src/commands/message/sign.rs b/cmd/soroban-cli/src/commands/message/sign.rs index a56c604a2d..392a86d251 100644 --- a/cmd/soroban-cli/src/commands/message/sign.rs +++ b/cmd/soroban-cli/src/commands/message/sign.rs @@ -67,7 +67,7 @@ pub struct Cmd { #[arg(long, help_heading = HEADING_SIGNING)] /// If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - pub hd_path: Option, + pub hd_path: Option, #[command(flatten)] pub locator: locator::Args, diff --git a/cmd/soroban-cli/src/commands/message/verify.rs b/cmd/soroban-cli/src/commands/message/verify.rs index 3eb5f665a8..5cdcaebc88 100644 --- a/cmd/soroban-cli/src/commands/message/verify.rs +++ b/cmd/soroban-cli/src/commands/message/verify.rs @@ -68,7 +68,7 @@ pub struct Cmd { /// If public key identity is a seed phrase use this hd path, default is 0 #[arg(long)] - pub hd_path: Option, + pub hd_path: Option, #[command(flatten)] pub locator: locator::Args, diff --git a/cmd/soroban-cli/src/config/address.rs b/cmd/soroban-cli/src/config/address.rs index 39e95b6b8b..ef037c45b3 100644 --- a/cmd/soroban-cli/src/config/address.rs +++ b/cmd/soroban-cli/src/config/address.rs @@ -66,7 +66,7 @@ impl UnresolvedMuxedAccount { pub fn resolve_muxed_account( &self, locator: &locator::Args, - hd_path: Option, + hd_path: Option, ) -> Result { match self { UnresolvedMuxedAccount::Resolved(muxed_account) => Ok(muxed_account.clone()), diff --git a/cmd/soroban-cli/src/config/key.rs b/cmd/soroban-cli/src/config/key.rs index f3da43be29..41de674479 100644 --- a/cmd/soroban-cli/src/config/key.rs +++ b/cmd/soroban-cli/src/config/key.rs @@ -30,7 +30,7 @@ pub enum Key { } impl Key { - pub fn muxed_account(&self, hd_path: Option) -> Result { + pub fn muxed_account(&self, hd_path: Option) -> Result { let bytes = match self { Key::Secret(secret) => secret.public_key(hd_path)?.0, Key::PublicKey(Public(key)) => key.0, @@ -49,7 +49,7 @@ impl Key { pub fn private_key( &self, - hd_path: Option, + hd_path: Option, ) -> Result { match self { Key::Secret(secret) => Ok(secret.private_key(hd_path)?), diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index 746818279c..32b107303d 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -313,7 +313,7 @@ impl Args { pub fn read_key_with_secure_store_cache( &self, key_or_name: &str, - hd_path: Option, + hd_path: Option, ) -> Result { if let Ok(literal) = key_or_name.parse::() { return Ok(literal); @@ -362,7 +362,7 @@ impl Args { pub fn get_secret_key_with_hd_path( &self, key_or_name: &str, - hd_path: Option, + hd_path: Option, ) -> Result { let key = self .read_key_with_secure_store_cache(key_or_name, hd_path) @@ -379,7 +379,7 @@ impl Args { pub fn get_public_key( &self, key_or_name: &str, - hd_path: Option, + hd_path: Option, ) -> Result { Ok(self.read_key(key_or_name)?.muxed_account(hd_path)?) } diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index 17743eb2cf..b980b447bb 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -197,7 +197,7 @@ impl Args { .into()) } - pub fn hd_path(&self) -> Option { + pub fn hd_path(&self) -> Option { self.sign_with.hd_path } } diff --git a/cmd/soroban-cli/src/config/sc_address.rs b/cmd/soroban-cli/src/config/sc_address.rs index b5aecfff40..07159af5da 100644 --- a/cmd/soroban-cli/src/config/sc_address.rs +++ b/cmd/soroban-cli/src/config/sc_address.rs @@ -44,7 +44,7 @@ impl UnresolvedScAddress { self, locator: &locator::Args, network_passphrase: &str, - hd_path: Option, + hd_path: Option, ) -> Result { let alias = match self { UnresolvedScAddress::Resolved(addr) => return Ok(addr), diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index d7402db73b..0f0e9638e6 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -34,12 +34,8 @@ pub enum Error { SecureStoreDoesNotRevealSecretKey, #[error(transparent)] Ledger(#[from] signer::ledger::Error), - #[error( - "--hd-path {requested} does not match the path stored on this Ledger identity ({cached})" - )] - LedgerHdPathMismatch { cached: usize, requested: usize }, - #[error("--hd-path {0} is out of range for a Ledger account index")] - HdPathOutOfRange(usize), + #[error("--hd-path is fixed at the time a Ledger identity is added; pass `--ledger --hd-path N` to inspect another path on the device")] + LedgerHdPathFixed, } #[derive(Debug, clap::Args, Clone)] @@ -75,7 +71,7 @@ pub enum Secret { // intended account without re-passing the flag. Optional for backwards // compatibility with files written before this field existed. #[serde(default, skip_serializing_if = "Option::is_none")] - hd_path: Option, + hd_path: Option, }, // Hardware-wallet identity. The required `hardware` field tags the device // kind (currently only `ledger`) and disambiguates this variant under @@ -86,7 +82,7 @@ pub enum Secret { hardware: HardwareKind, public_key: String, #[serde(default, skip_serializing_if = "Option::is_none")] - hd_path: Option, + hd_path: Option, }, SecureStore { entry_name: String, @@ -96,7 +92,7 @@ pub enum Secret { #[serde(default, skip_serializing_if = "Option::is_none")] public_key: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - hd_path: Option, + hd_path: Option, }, } @@ -155,7 +151,7 @@ impl From for Secret { } impl Secret { - pub fn private_key(&self, index: Option) -> Result { + pub fn private_key(&self, index: Option) -> Result { Ok(match self { Secret::SecretKey { secret_key } => PrivateKey::from_string(secret_key)?, Secret::SeedPhrase { @@ -163,7 +159,7 @@ impl Secret { hd_path, } => PrivateKey::from_payload( &sep5::SeedPhrase::from_str(seed_phrase)? - .from_path_index(index.or(*hd_path).unwrap_or_default(), None)? + .from_path_index(index.or(*hd_path).unwrap_or_default() as usize, None)? .private() .0, )?, @@ -174,7 +170,7 @@ impl Secret { }) } - pub fn public_key(&self, index: Option) -> Result { + pub fn public_key(&self, index: Option) -> Result { match self { Secret::SecureStore { entry_name, @@ -188,15 +184,9 @@ impl Secret { } Ok(secure_store::get_public_key(entry_name, effective)?) } - Secret::Ledger { - public_key, - hd_path: cached_hd_path, - .. - } => { - let cached = cached_hd_path.unwrap_or_default(); - let requested = index.unwrap_or(cached); - if cached != requested { - return Err(Error::LedgerHdPathMismatch { cached, requested }); + Secret::Ledger { public_key, .. } => { + if index.is_some() { + return Err(Error::LedgerHdPathFixed); } Ok(PublicKey::from_string(public_key)?) } @@ -209,7 +199,7 @@ impl Secret { } } - pub fn signer(&self, hd_path: Option, print: Print) -> Result { + pub fn signer(&self, hd_path: Option, print: Print) -> Result { let kind = match self { Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => { let key = self.key_pair(hd_path)?; @@ -220,12 +210,11 @@ impl Secret { public_key, hd_path: cached_hd_path, } => { - let effective = hd_path.or(*cached_hd_path).unwrap_or_default(); - let hd_path: u32 = effective - .try_into() - .map_err(|_| Error::HdPathOutOfRange(effective))?; + if hd_path.is_some() { + return Err(Error::LedgerHdPathFixed); + } SignerKind::Ledger(LedgerEntry { - hd_path, + hd_path: cached_hd_path.unwrap_or_default(), public_key: Some(PublicKey::from_string(public_key)?), }) } @@ -247,7 +236,7 @@ impl Secret { Ok(Signer { kind, print }) } - pub fn key_pair(&self, index: Option) -> Result { + pub fn key_pair(&self, index: Option) -> Result { Ok(utils::into_signing_key(&self.private_key(index)?)) } @@ -262,8 +251,8 @@ impl Secret { // since the rest of the codebase uses `unwrap_or_default()` for hd_path. fn cached_public_key( cached: Option<&str>, - cached_hd_path: Option, - requested_hd_path: Option, + cached_hd_path: Option, + requested_hd_path: Option, ) -> Option { if cached_hd_path.unwrap_or_default() != requested_hd_path.unwrap_or_default() { return None; @@ -531,32 +520,35 @@ mod tests { } #[test] - fn test_ledger_public_key_rejects_mismatched_hd_path() { - // Caller asks for a different account index than the one cached on - // disk; returning the cached key would leak the wrong address. + fn test_ledger_public_key_rejects_caller_hd_path() { + // The hd-path on a Ledger alias is fixed at `keys add` time; any + // caller-supplied --hd-path should error rather than silently using + // the cached value or attempting to override it. Discovery on other + // paths goes through `--ledger --hd-path N` instead. let secret = Secret::Ledger { hardware: HardwareKind::Ledger, public_key: TEST_PUBLIC_KEY.to_string(), hd_path: Some(5), }; + assert!(matches!( + secret.public_key(Some(5)).unwrap_err(), + Error::LedgerHdPathFixed, + )); assert!(matches!( secret.public_key(Some(7)).unwrap_err(), - Error::LedgerHdPathMismatch { - cached: 5, - requested: 7 - }, + Error::LedgerHdPathFixed, )); } #[test] - fn test_ledger_public_key_treats_none_and_zero_as_equivalent() { + fn test_ledger_public_key_uses_cached_path_when_caller_passes_none() { let secret = Secret::Ledger { hardware: HardwareKind::Ledger, public_key: TEST_PUBLIC_KEY.to_string(), - hd_path: None, + hd_path: Some(5), }; assert_eq!( - secret.public_key(Some(0)).unwrap().to_string(), + secret.public_key(None).unwrap().to_string(), TEST_PUBLIC_KEY ); } diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index 1d21bd6dc0..b4a43d0626 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -48,7 +48,7 @@ pub struct Args { #[arg(long, conflicts_with = "sign_with_lab", help_heading = HEADING_SIGNING)] /// If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` - pub hd_path: Option, + pub hd_path: Option, #[allow(clippy::doc_markdown)] /// Sign with https://lab.stellar.org @@ -88,14 +88,9 @@ impl Args { print, } } else if self.sign_with_ledger { - let hd_path = self - .hd_path - .unwrap_or_default() - .try_into() - .unwrap_or_default(); Signer { kind: SignerKind::Ledger(LedgerEntry { - hd_path, + hd_path: self.hd_path.unwrap_or_default(), public_key: None, }), print, diff --git a/cmd/soroban-cli/src/signer/keyring.rs b/cmd/soroban-cli/src/signer/keyring.rs index 229a16a864..7b09bc1cf9 100644 --- a/cmd/soroban-cli/src/signer/keyring.rs +++ b/cmd/soroban-cli/src/signer/keyring.rs @@ -91,12 +91,12 @@ impl StellarEntry { fn use_key( &self, f: impl FnOnce(ed25519_dalek::SigningKey) -> Result, - hd_path: Option, + hd_path: Option, ) -> Result { // The underlying Mnemonic type is zeroized when dropped let mut key_bytes: [u8; 32] = { self.get_seed_phrase()? - .from_path_index(hd_path.unwrap_or_default(), None)? + .from_path_index(hd_path.unwrap_or_default() as usize, None)? .private() .0 }; @@ -111,7 +111,7 @@ impl StellarEntry { pub fn get_public_key( &self, - hd_path: Option, + hd_path: Option, ) -> Result { self.use_key( |keypair| { @@ -123,7 +123,7 @@ impl StellarEntry { ) } - pub fn sign_data(&self, data: &[u8], hd_path: Option) -> Result, Error> { + pub fn sign_data(&self, data: &[u8], hd_path: Option) -> Result, Error> { self.use_key( |keypair| { let signature = keypair.sign(data); diff --git a/cmd/soroban-cli/src/signer/mod.rs b/cmd/soroban-cli/src/signer/mod.rs index 0bec9b3f07..87b93bab2d 100644 --- a/cmd/soroban-cli/src/signer/mod.rs +++ b/cmd/soroban-cli/src/signer/mod.rs @@ -351,7 +351,7 @@ impl Lab { pub struct SecureStoreEntry { pub name: String, - pub hd_path: Option, + pub hd_path: Option, pub public_key: Option, } diff --git a/cmd/soroban-cli/src/signer/secure_store.rs b/cmd/soroban-cli/src/signer/secure_store.rs index 45c99ccf6d..bac142bb7f 100644 --- a/cmd/soroban-cli/src/signer/secure_store.rs +++ b/cmd/soroban-cli/src/signer/secure_store.rs @@ -32,7 +32,7 @@ mod secure_store_impl { use super::{Error, Print, PublicKey, Secret, SeedPhrase, StellarEntry, ENTRY_PREFIX}; const ENTRY_SERVICE: &str = "org.stellar.cli"; - pub fn get_public_key(entry_name: &str, index: Option) -> Result { + pub fn get_public_key(entry_name: &str, index: Option) -> Result { let entry = StellarEntry::new(entry_name)?; Ok(entry.get_public_key(index)?) } @@ -46,7 +46,7 @@ mod secure_store_impl { print: &Print, name: &str, seed_phrase: &SeedPhrase, - hd_path: Option, + hd_path: Option, overwrite: bool, ) -> Result { // secure_store:org.stellar.cli- @@ -57,7 +57,7 @@ mod secure_store_impl { let public_key_bytes = seed_phrase .clone() - .from_path_index(hd_path.unwrap_or_default(), None)? + .from_path_index(hd_path.unwrap_or_default() as usize, None)? .public() .0; let public_key = PublicKey(public_key_bytes).to_string(); @@ -71,7 +71,7 @@ mod secure_store_impl { pub fn sign_tx_data( entry_name: &str, - hd_path: Option, + hd_path: Option, data: &[u8], ) -> Result, Error> { let entry = StellarEntry::new(entry_name)?; @@ -83,7 +83,7 @@ mod secure_store_impl { mod secure_store_impl { use super::{Error, Print, PublicKey, Secret, SeedPhrase}; - pub fn get_public_key(_entry_name: &str, _index: Option) -> Result { + pub fn get_public_key(_entry_name: &str, _index: Option) -> Result { Err(Error::FeatureNotEnabled) } @@ -95,7 +95,7 @@ mod secure_store_impl { _print: &Print, _name: &str, _seed_phrase: &SeedPhrase, - _hd_path: Option, + _hd_path: Option, _overwrite: bool, ) -> Result { Err(Error::FeatureNotEnabled) @@ -103,7 +103,7 @@ mod secure_store_impl { pub fn sign_tx_data( _entry_name: &str, - _hd_path: Option, + _hd_path: Option, _data: &[u8], ) -> Result, Error> { Err(Error::FeatureNotEnabled) From 4739d434f7e57db27b8629df54b6e11fc83ab1e9 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Fri, 8 May 2026 10:00:32 -0700 Subject: [PATCH 3/3] Add emulator test for Ledger auth signing. --- cmd/crates/soroban-test/tests/it/emulator.rs | 91 ++++++++++++++++++- .../src/emulator_test_support/speculos.rs | 2 +- .../src/emulator_test_support/util.rs | 2 +- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/cmd/crates/soroban-test/tests/it/emulator.rs b/cmd/crates/soroban-test/tests/it/emulator.rs index b959c541c2..8ca9b45cd5 100644 --- a/cmd/crates/soroban-test/tests/it/emulator.rs +++ b/cmd/crates/soroban-test/tests/it/emulator.rs @@ -1,6 +1,6 @@ use stellar_ledger::Blob; -use soroban_test::{AssertExt, TestEnv}; +use soroban_test::{AssertExt, TestEnv, Wasm}; use std::sync::Arc; use stellar_ledger::emulator_test_support::*; @@ -12,6 +12,8 @@ use soroban_cli::{ use test_case::test_case; +const HELLO_WORLD: &Wasm = &Wasm::Custom("test-wasms", "test_hello_world"); + #[test_case("nanos", 0; "when the device is NanoS")] #[test_case("nanox", 1; "when the device is NanoX")] #[test_case("nanosp", 2; "when the device is NanoS Plus")] @@ -88,3 +90,90 @@ async fn test_signer(ledger_device_model: &str, hd_path: u32) { ) .unwrap(); } + +// Mirrors `invoke_auth_with_non_source_identity` from the integration tests: +// invoke a contract whose `auth(addr, world)` calls `addr.require_auth()`, +// where the auth identity (`testone`) is a Ledger-backed alias and the +// transaction source (`test`) is a regular keypair. Exercises the Soroban +// auth-entry signing path through the Ledger device. +#[test_case("nanos", 0; "when the device is NanoS")] +#[test_case("nanox", 1; "when the device is NanoX")] +#[test_case("nanosp", 2; "when the device is NanoS Plus")] +#[tokio::test] +async fn invoke_auth_with_ledger_identity(ledger_device_model: &str, hd_path: u32) { + 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(); + + sandbox + .new_assert_cmd("keys") + .arg("fund") + .arg("test") + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("add") + .arg("testone") + .arg("--ledger") + .arg("--hd-path") + .arg(hd_path.to_string()) + .env("SPECULOS_PORT", host_port.to_string()) + .assert() + .success(); + + let addr = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("testone") + .assert() + .success() + .stdout_as_str(); + + let id = sandbox + .new_assert_cmd("contract") + .arg("deploy") + .arg("--source") + .arg("test") + .arg("--wasm") + .arg(HELLO_WORLD.path()) + .arg("--ignore-checks") + .assert() + .success() + .stdout_as_str(); + + let invoke = tokio::task::spawn_blocking({ + let sandbox = Arc::clone(&sandbox); + let id = id.clone(); + let addr = addr.clone(); + move || { + let stdout = sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--source") + .arg("test") + .arg("--id") + .arg(&id) + .arg("--") + .arg("auth") + .arg("--addr") + .arg("testone") + .arg("--world=world") + .env("SPECULOS_PORT", host_port.to_string()) + .assert() + .success() + .stdout_as_str(); + assert_eq!(stdout, format!("\"{addr}\"")); + } + }); + + let approve = tokio::task::spawn(approve_tx_hash_signature( + ui_host_port, + ledger_device_model.to_string(), + )); + + invoke.await.unwrap(); + approve.await.unwrap(); +} 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 f8b95c13ac..81a11b43a4 100644 --- a/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs @@ -81,7 +81,7 @@ impl FromStr for DeviceModel { "nanos" => Ok(DeviceModel::NanoS), "nanosp" => Ok(DeviceModel::NanoSP), "nanox" => Ok(DeviceModel::NanoX), - _ => Err(format!("Unsupported device model: {}", s)), + _ => Err(format!("Unsupported device model: {s}")), } } } 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 c4100e8d1f..053e7e3f3c 100644 --- a/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs @@ -47,7 +47,7 @@ pub async fn click(ui_host_port: u16, url: &str) { let current_events = get_emulator_events(ui_host_port).await; if !(previous_events == current_events) { - screen_has_changed = true + screen_has_changed = true; } }