Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4145,7 +4145,7 @@ Encode a transaction envelope from JSON to XDR

Decode and encode XDR

**Usage:** `stellar xdr [CHANNEL] <COMMAND>`
**Usage:** `stellar xdr <COMMAND>`

###### **Subcommands:**

Expand All @@ -4158,14 +4158,6 @@ Decode and encode XDR
- `xfile` — Preprocess XDR .x files
- `version` — Print version information

###### **Arguments:**

- `<CHANNEL>` — Channel of XDR to operate on

Default value: `+curr`

Possible values: `+curr`, `+next`

## `stellar xdr types`

View information about types
Expand Down
1 change: 1 addition & 0 deletions cmd/crates/soroban-test/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ mod message;
mod plugin;
mod rpc_provider;
mod strkey;
mod tx;
mod util;
mod version;
51 changes: 51 additions & 0 deletions cmd/crates/soroban-test/tests/it/tx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use soroban_test::{AssertExt, TestEnv};

// Transaction envelope from https://github.com/stellar/stellar-cli/issues/2455.
const TX_ENVELOPE: &str = "AAAAAgAAAAAnEWb4nxMsQhdnS16MqBxwItF3X/JNRkfxu8eyEQyfegAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAABAAAAABk++lrW4HlZwaWwYdYvJPCil1ibyI/VTH6WKda2sOC+AAAAAAAAAAAAAABkAAAAAAAAAAA=";
const SECRET_KEY: &str = "SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW";

// `tx sign` only mixes the network passphrase into the transaction hash, so it
// must not require an RPC URL (regression test for #2455).
#[tokio::test]
async fn tx_sign_requires_only_network_passphrase() {
let sandbox = &TestEnv::new();

let output = sandbox
.new_assert_cmd("tx")
.args([
"sign",
TX_ENVELOPE,
"--network-passphrase",
"specified manually",
"--sign-with-key",
SECRET_KEY,
])
.assert()
.success()
.stdout_as_str();

// A signed envelope is longer than the unsigned one it was built from.
assert!(output.trim().len() > TX_ENVELOPE.len());
}

// Same expectation for `tx hash`, which also only needs the passphrase.
#[tokio::test]
async fn tx_hash_requires_only_network_passphrase() {
let sandbox = &TestEnv::new();

let output = sandbox
.new_assert_cmd("tx")
.args([
"hash",
TX_ENVELOPE,
"--network-passphrase",
"specified manually",
])
.assert()
.success()
.stdout_as_str();

let hash = output.trim();
assert_eq!(hash.len(), 64, "expected a 32-byte hex hash, got: {hash}");
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
2 changes: 1 addition & 1 deletion cmd/soroban-cli/src/commands/tx/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub struct Cmd {
impl Cmd {
pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
let tx = super::xdr::unwrap_envelope_v1(super::xdr::tx_envelope_from_input(&self.tx_xdr)?)?;
let network = &self.network.get(&global_args.locator)?;
let network = &self.network.get_no_rpc(&global_args.locator)?;
println!(
"{}",
hex::encode(transaction_hash(&tx, &network.network_passphrase)?)
Expand Down
2 changes: 1 addition & 1 deletion cmd/soroban-cli/src/commands/tx/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ impl Cmd {
.sign_tx_env(
&tx_env,
&self.locator,
&self.network.get(&self.locator)?,
&self.network.get_no_rpc(&self.locator)?,
global_args.quiet,
None,
)
Expand Down
91 changes: 91 additions & 0 deletions cmd/soroban-cli/src/config/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ pub struct Args {

impl Args {
pub fn get(&self, locator: &locator::Args) -> Result<Network, Error> {
self.resolve(locator, /* require_rpc */ true)
}

/// Resolve the network for commands that only need the passphrase
/// (e.g. `tx sign`, `tx hash`) and never contact an RPC server.
///
/// When only a passphrase is supplied, the returned `Network` has an empty
/// `rpc_url`; callers of this method MUST NOT use `rpc_client()`.
pub fn get_no_rpc(&self, locator: &locator::Args) -> Result<Network, Error> {
self.resolve(locator, /* require_rpc */ false)
}

fn resolve(&self, locator: &locator::Args, require_rpc: bool) -> Result<Network, Error> {
match (
self.network.as_deref(),
self.rpc_url.clone(),
Expand All @@ -108,6 +121,13 @@ impl Args {
Ok(DEFAULTS.get(DEFAULT_NETWORK_KEY).unwrap().into())
}
(_, Some(_), None) => Err(Error::MissingNetworkPassphrase),
// Signing-only commands don't need an RPC URL, so accept a
// passphrase on its own and leave `rpc_url` empty.
(_, None, Some(network_passphrase)) if !require_rpc => Ok(Network {
rpc_url: String::new(),
rpc_headers: Vec::new(),
network_passphrase,
}),
(_, None, Some(_)) => Err(Error::MissingRpcUrl),
(Some(network), None, None) => Ok(locator.read_network(network)?),
(_, Some(rpc_url), Some(network_passphrase)) => {
Expand Down Expand Up @@ -549,6 +569,77 @@ mod tests {
assert_eq!(network.rpc_url, "https://soroban-testnet.stellar.org");
}

#[test]
fn test_get_no_rpc_accepts_passphrase_only() {
use super::super::locator;

let args = Args {
rpc_url: None,
rpc_headers: Vec::new(),
network_passphrase: Some("specified manually".to_string()),
network: None,
};

let network = args
.get_no_rpc(&locator::Args::default())
.expect("passphrase-only network should resolve for signing-only commands");
assert_eq!(network.network_passphrase, "specified manually");
assert_eq!(network.rpc_url, "");
assert!(network.rpc_headers.is_empty());
}

#[test]
fn test_get_no_rpc_still_requires_passphrase_when_rpc_given() {
use super::super::locator;

let args = Args {
rpc_url: Some("https://example.com".to_string()),
rpc_headers: Vec::new(),
network_passphrase: None,
network: None,
};

let err = args.get_no_rpc(&locator::Args::default()).expect_err(
"rpc without passphrase should still error, even for signing-only commands",
);
assert!(matches!(err, Error::MissingNetworkPassphrase));
}

#[test]
fn test_get_no_rpc_preserves_rpc_url_when_both_given() {
use super::super::locator;

let args = Args {
rpc_url: Some("https://example.com".to_string()),
rpc_headers: Vec::new(),
network_passphrase: Some("specified manually".to_string()),
network: None,
};

let network = args.get_no_rpc(&locator::Args::default()).unwrap();
assert_eq!(network.rpc_url, "https://example.com");
assert_eq!(network.network_passphrase, "specified manually");
}

#[test]
fn test_get_strict_still_requires_rpc_url_with_passphrase_only() {
use super::super::locator;

let args = Args {
rpc_url: None,
rpc_headers: Vec::new(),
network_passphrase: Some("specified manually".to_string()),
network: None,
};

// The strict resolver used by RPC commands must keep rejecting a
// passphrase-only invocation.
let err = args
.get(&locator::Args::default())
.expect_err("strict get() must still require an rpc-url with passphrase-only args");
assert!(matches!(err, Error::MissingRpcUrl));
}

#[tokio::test]
async fn test_user_config_default_overrides_automatic_testnet() {
use super::super::locator;
Expand Down