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
1 change: 1 addition & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,7 @@ stellar contract invoke ... -- --help

- `--id <CONTRACT_ID>` — Contract ID to invoke
- `--is-view` — ⚠️ Deprecated, use `--send=no`. View the result simulating and do not sign and submit transaction
- `--invoke-contract-args <INVOKE_CONTRACT_ARGS>` — (Optional) Base-64 InvokeContractArgs envelope XDR or file containing XDR to decode. If used, function name and arguments must be omitted, as this value will be used instead
- `-s`, `--source-account <SOURCE_ACCOUNT>` [alias: `source`] — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` was NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail
- `--sign-with-key <SIGN_WITH_KEY>` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path
- `--hd-path <HD_PATH>` — 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`
Expand Down
56 changes: 53 additions & 3 deletions cmd/soroban-cli/src/commands/contract/arg_parsing.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::commands::contract::arg_parsing::Error::HelpMessage;
use crate::commands::contract::arg_parsing::Error::{CannotParseXDR, HelpMessage};
use crate::commands::contract::deploy::wasm::CONSTRUCTOR_FUNCTION_NAME;
use crate::commands::txn_result::TxnResult;
use crate::config::{self, sc_address, UnresolvedScAddress};
Expand All @@ -16,8 +16,10 @@ use std::convert::TryInto;
use std::env;
use std::ffi::OsString;
use std::fmt::Debug;
use std::path::PathBuf;
use stellar_xdr::curr::ContractId;
use std::fs::File;
use std::io::{Cursor, Read};
use std::path::{Path, PathBuf};
use stellar_xdr::curr::{ContractId, Limited, Limits, ReadXdr, SkipWhitespace};

#[derive(thiserror::Error, Debug)]
pub enum Error {
Expand Down Expand Up @@ -77,6 +79,10 @@ pub enum Error {
HelpMessage(String),
#[error(transparent)]
Signer(#[from] signer::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("cannot parse XDR: {error}")]
CannotParseXDR { error: xdr::Error },
}

pub type HostFunctionParameters = (String, Spec, InvokeContractArgs, Vec<Signer>);
Expand Down Expand Up @@ -126,6 +132,50 @@ async fn build_host_function_parameters_with_filter(
Ok((function, spec, invoke_args, signers))
}

pub async fn build_host_function_parameters_from_string_xdr(
string_xdr: &OsString,
spec_entries: &[ScSpecEntry],
config: &config::Args,
) -> Result<HostFunctionParameters, Error> {
let spec = Spec(Some(spec_entries.to_vec()));
let invoke_args = invoke_contract_args_from_input(string_xdr)?;
let mut signers = Vec::<Signer>::new();
let args = invoke_args.args.to_vec();
for x in args {
let signer = match x {
ScVal::Address(addr) => {
let resolved = resolve_address(addr.to_string().as_str(), config)?;
resolve_signer(resolved.as_str(), config).await
}
_ => None,
};
if let Some(signer) = signer {
signers.push(signer);
}
}

Ok((
invoke_args.function_name.to_string(),
spec,
invoke_args,
signers,
))
}

fn invoke_contract_args_from_input(input: &OsString) -> Result<InvokeContractArgs, Error> {
let read: &mut dyn Read = {
let exist = Path::new(input).try_exists();
if let Ok(true) = exist {
&mut File::open(input)?
} else {
&mut Cursor::new(input.clone().into_encoded_bytes())
}
};

let mut lim = Limited::new(SkipWhitespace::new(read), Limits::none());
Comment on lines +166 to +175
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invoke_contract_args_from_input returns &mut dyn Read pointing at temporaries created in a block (&mut File::open(...) / &mut Cursor::new(...)). This pattern can fail to compile with “temporary value dropped while borrowed” (temporary lifetime extension doesn’t reliably apply through a block), and it’s fragile to refactors. Prefer an owned reader (e.g., Box<dyn Read> or an enum) and then pass &mut *reader into SkipWhitespace::new/Limited::new.

Suggested change
let read: &mut dyn Read = {
let exist = Path::new(input).try_exists();
if let Ok(true) = exist {
&mut File::open(input)?
} else {
&mut Cursor::new(input.clone().into_encoded_bytes())
}
};
let mut lim = Limited::new(SkipWhitespace::new(read), Limits::none());
let mut reader: Box<dyn Read> = {
let exist = Path::new(input).try_exists();
if let Ok(true) = exist {
Box::new(File::open(input)?)
} else {
Box::new(Cursor::new(input.clone().into_encoded_bytes()))
}
};
let mut lim = Limited::new(SkipWhitespace::new(&mut *reader), Limits::none());

Copilot uses AI. Check for mistakes.
InvokeContractArgs::read_xdr_base64_to_end(&mut lim).map_err(|e| CannotParseXDR { error: e })
}

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New XDR-based invocation path (build_host_function_parameters_from_string_xdr / invoke_contract_args_from_input) isn’t covered by tests in this module. Since this file already has a #[cfg(test)] suite, please add tests for parsing from (1) a base64 string and (2) a file path, plus an invalid-XDR case to assert the CannotParseXDR error formatting.

Suggested change
#[cfg(test)]
mod xdr_invoke_tests {
use super::{invoke_contract_args_from_input, Error};
use crate::xdr::{self, Hash, InvokeContractArgs, ScAddress, ScVal, ScVec, Symbol, WriteXdr};
use std::ffi::OsString;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn make_test_invoke_args() -> InvokeContractArgs {
InvokeContractArgs {
contract_address: ScAddress::Contract(Hash([0; 32])),
function_name: Symbol::try_from("test_func").unwrap(),
args: ScVec(Some(vec![ScVal::I32(7)])),
}
}
#[test]
fn parse_invoke_args_from_base64_string() {
let invoke = make_test_invoke_args();
let b64 = invoke.to_xdr_base64().expect("serialize to base64");
let input = OsString::from(b64);
let parsed = invoke_contract_args_from_input(&input).expect("parse from base64 string");
assert_eq!(parsed.function_name, invoke.function_name);
assert_eq!(parsed.args.0.as_ref().unwrap().len(), 1);
assert_eq!(parsed.args.0.as_ref().unwrap()[0], ScVal::I32(7));
}
#[test]
fn parse_invoke_args_from_file_path() {
let invoke = make_test_invoke_args();
let b64 = invoke.to_xdr_base64().expect("serialize to base64");
let mut path = std::env::temp_dir();
path.push("soroban_invoke_args_test.xdr");
{
let mut file = File::create(&path).expect("create temp file");
file.write_all(b64.as_bytes())
.expect("write base64 XDR to temp file");
}
let input = OsString::from(path.as_os_str());
let parsed = invoke_contract_args_from_input(&input).expect("parse from file path");
assert_eq!(parsed.function_name, invoke.function_name);
assert_eq!(parsed.args.0.as_ref().unwrap().len(), 1);
assert_eq!(parsed.args.0.as_ref().unwrap()[0], ScVal::I32(7));
}
#[test]
fn invalid_xdr_reports_cannot_parse_xdr_error() {
let input = OsString::from("not-a-valid-base64-xdr");
let err = invoke_contract_args_from_input(&input).expect_err("expected parse error");
match err {
Error::CannotParseXDR { .. } => {
let msg = format!("{err}");
// Ensure the formatted error message clearly indicates an XDR parse failure.
assert!(
msg.contains("XDR"),
"expected error message to mention XDR, got: {msg}"
);
}
other => panic!("expected CannotParseXDR, got: {other:?}"),
}
}
}

Copilot uses AI. Check for mistakes.
fn build_clap_command(spec: &Spec, filter_constructor: bool) -> Result<clap::Command, Error> {
let mut cmd = clap::Command::new(running_cmd())
.no_binary_name(true)
Expand Down
38 changes: 32 additions & 6 deletions cmd/soroban-cli/src/commands/contract/invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use soroban_spec::read::FromWasmError;
use super::super::events;
use super::arg_parsing;
use crate::assembled::Assembled;
use crate::commands::contract::arg_parsing::build_host_function_parameters_from_string_xdr;
use crate::commands::tx::fetch;
use crate::log::extract_events;
use crate::print::Print;
Expand Down Expand Up @@ -55,9 +56,18 @@ pub struct Cmd {
#[arg(long, env = "STELLAR_INVOKE_VIEW")]
pub is_view: bool,

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new --invoke-contract-args option is missing a doc comment/help text. Please document what value it expects (base64 XDR string vs path to a file containing base64 XDR) and, if applicable, whether whitespace/newlines are allowed.

Suggested change
/// Raw base64-encoded XDR `InvokeContractArgs` to invoke directly, instead of specifying a
/// function name and arguments. The value must be a single base64 string (no whitespace or
/// newlines) and is interpreted as the XDR payload itself, not as a path to a file.

Copilot uses AI. Check for mistakes.
/// (Optional) Base-64 InvokeContractArgs envelope XDR or file containing XDR to decode. If used,
/// function name and arguments must be omitted, as this value will be used instead.
#[arg(long, conflicts_with = "CONTRACT_FN_AND_ARGS")]
pub invoke_contract_args: Option<OsString>,

/// Function name as subcommand, then arguments for that function as `--arg-name value`
#[arg(last = true, id = "CONTRACT_FN_AND_ARGS")]
pub slop: Vec<OsString>,
#[arg(
last = true,
id = "CONTRACT_FN_AND_ARGS",
conflicts_with = "invoke_contract_args"
)]
pub slop: Option<Vec<OsString>>,
Comment on lines +61 to +70
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description still contains TODO placeholders for the What/Why/Known limitations sections. Please fill these in so reviewers/users understand the intent and any constraints of adding --invoke-contract-args support.

Copilot uses AI. Check for mistakes.

#[command(flatten)]
pub config: config::Args,
Expand Down Expand Up @@ -268,8 +278,13 @@ impl Cmd {
let spec_entries = self.spec_entries()?;

if let Some(spec_entries) = &spec_entries {
// For testing wasm arg parsing
build_host_function_parameters(&contract_id, &self.slop, spec_entries, config).await?;
if let Some(slop) = &self.slop {
// For testing wasm arg parsing
build_host_function_parameters(&contract_id, slop, spec_entries, config).await?;
} else if self.invoke_contract_args.is_none() {
// For giving a nice error message if --invoke-contract-args was not provided and slop not used
build_host_function_parameters(&contract_id, &[], spec_entries, config).await?;
}
}

let client = network.rpc_client()?;
Expand All @@ -294,8 +309,19 @@ impl Cmd {
.await
.map_err(Error::from)?;

let params =
build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config).await?;
let params = if let Some(slop) = &self.slop {
build_host_function_parameters(&contract_id, slop, &spec_entries, config).await?
} else if let Some(invoke_contract_args) = &self.invoke_contract_args {
build_host_function_parameters_from_string_xdr(
invoke_contract_args,
&spec_entries,
config,
)
.await?
} else {
// For giving a nice error message if --invoke-contract-args was not provided and slop not used
build_host_function_parameters(&contract_id, &[], &spec_entries, config).await?
};

let (function, spec, host_function_params, signers) = params;

Expand Down
30 changes: 1 addition & 29 deletions cmd/soroban-cli/src/commands/tx/xdr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::fs::File;
use std::io::{stdin, Read};
use std::io::{Cursor, IsTerminal};
use std::path::Path;
use stellar_xdr::curr::Limited;
use stellar_xdr::curr::{Limited, SkipWhitespace};

#[derive(Debug, thiserror::Error)]
pub enum Error {
Expand Down Expand Up @@ -41,34 +41,6 @@ pub fn tx_envelope_from_input(input: &Option<OsString>) -> Result<TransactionEnv
Ok(TransactionEnvelope::read_xdr_base64_to_end(&mut lim)?)
}

// TODO: use SkipWhitespace from rs-stellar-xdr once it's updated to 23.0
pub struct SkipWhitespace<R: Read> {
pub inner: R,
}

impl<R: Read> SkipWhitespace<R> {
pub fn new(inner: R) -> Self {
SkipWhitespace { inner }
}
}

impl<R: Read> Read for SkipWhitespace<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.inner.read(buf)?;

let mut written = 0;
for read in 0..n {
if !buf[read].is_ascii_whitespace() {
buf[written] = buf[read];
written += 1;
}
}

Ok(written)
}
}
//

pub fn unwrap_envelope_v1(tx_env: TransactionEnvelope) -> Result<Transaction, Error> {
let TransactionEnvelope::Tx(TransactionV1Envelope { tx, .. }) = tx_env else {
return Err(Error::OnlyTransactionV1Supported);
Expand Down
Loading