diff --git a/cmd/soroban-cli/src/commands/skill/SKILL.md b/cmd/soroban-cli/src/commands/skill/SKILL.md index 80bf2d8275..049cba95ae 100644 --- a/cmd/soroban-cli/src/commands/skill/SKILL.md +++ b/cmd/soroban-cli/src/commands/skill/SKILL.md @@ -41,6 +41,7 @@ After `stellar keys use`, other commands sign and pay with that identity, so you - Clear the default: `stellar keys unset` Only pass `--source ` when a single command needs to override the default identity. +- Predictable auth signatures: pass `--signature-expiration-ledger ` (or env `STELLAR_SIGNATURE_EXPIRATION_LEDGER`) to explicitly set the authorization expiration sequence instead of querying RPC `getLatestLedger`. ## Contracts: use aliases, don't stash contract ids in env vars diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index f492fb2809..5ae5acadc4 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -153,9 +153,13 @@ impl Args { print: &Print, ) -> Result, Error> { let network = self.get_network()?; - let client = network.rpc_client()?; - let latest_ledger = client.get_latest_ledger().await?.sequence; - let seq_num = latest_ledger + 60; // ~ 5 min + let seq_num = if let Some(exp) = self.sign_with.signature_expiration_ledger { + exp + } else { + let client = network.rpc_client()?; + let latest_ledger = client.get_latest_ledger().await?.sequence; + latest_ledger + 60 // ~ 5 min + }; Ok(signer::sign_soroban_authorizations( tx, signers, diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index 6ea1bb560c..b94018649a 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -73,6 +73,14 @@ pub struct Args { /// Sign without prompting for approval. Only applies to signatures that require user approval, like non-root Soroban auth entries. #[arg(long, help_heading = HEADING_SIGNING)] pub auto_sign: bool, + + /// Explicit signature expiration ledger sequence for Soroban authorization signatures. If not set, defaults to latest ledger + 60 (~5 minutes). + #[arg( + long, + env = "STELLAR_SIGNATURE_EXPIRATION_LEDGER", + help_heading = HEADING_SIGNING + )] + pub signature_expiration_ledger: Option, } impl Args { @@ -115,3 +123,43 @@ impl Args { Ok(signer.sign_tx_env(tx, network).await?) } } + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + #[derive(Parser, Debug)] + struct TestCli { + #[command(flatten)] + sign_with: Args, + } + + #[test] + fn test_signature_expiration_ledger_arg_parsing() { + // 1. Defaults to None + let parsed = TestCli::try_parse_from(["test"]).unwrap(); + assert_eq!(parsed.sign_with.signature_expiration_ledger, None); + + // 2. Parsed from command-line argument + let parsed = TestCli::try_parse_from(["test", "--signature-expiration-ledger", "123456"]).unwrap(); + assert_eq!(parsed.sign_with.signature_expiration_ledger, Some(123456)); + + // 3. Parsed from environment variable + let env_key = "STELLAR_SIGNATURE_EXPIRATION_LEDGER"; + let prev = std::env::var(env_key).ok(); + std::env::set_var(env_key, "654321"); + let parsed = TestCli::try_parse_from(["test"]).unwrap(); + assert_eq!(parsed.sign_with.signature_expiration_ledger, Some(654321)); + + // Command line argument overrides env var + let parsed = TestCli::try_parse_from(["test", "--signature-expiration-ledger", "789012"]).unwrap(); + assert_eq!(parsed.sign_with.signature_expiration_ledger, Some(789012)); + + if let Some(p) = prev { + std::env::set_var(env_key, p); + } else { + std::env::remove_var(env_key); + } + } +}