Skip to content

feat: invoke from XDR args#2460

Open
ifropc wants to merge 2 commits intostellar:mainfrom
ifropc:invoke-xdr
Open

feat: invoke from XDR args#2460
ifropc wants to merge 2 commits intostellar:mainfrom
ifropc:invoke-xdr

Conversation

@ifropc
Copy link
Copy Markdown
Contributor

@ifropc ifropc commented Mar 23, 2026

What

Add ability to call contract invoke with base64 encoded InvokeContractArgs

Why

This is very useful for invoking contracts when arguments are already in JSON or XDR
One particular case is replaying transactions (e.g. after testnet reset one can simply re-apply all previous transactions by simply reading the chain and storing all XDRs)
This is much more simple that writing custom scripts where invoker must know all argument names, and correctly transform JSON XDR into the format the cli expects

Known limitations

Blocked by #2459 as all address arguments are regular public keys, so it's impossible to properly invoke (with sign and sending) such transactions

Testing

Example:

  1. Encode arguments into XDR
image
AAAAAUX05neMv38MV/DiJGmlSlHA3w+b3GqbZnnPr7mPVHm+AAAABmRlcGxveQAAAAAABgAAAA4AAAADYWNjAAAAAAEAAAAOAAAAGWFjY190ZXN0X25ld192ZXJzaW9uX21vcmUAAAAAAAASAAAAAAAAAAAebE8Ewg9uzvHRAl+UmvP0kixsyp238kkz0zOy+91FeQAAAAEAAAAB
  1. > cargo run contract invoke --source-account alice --id CBC7JZTXRS7X6DCX6DRCI2NFJJI4BXYPTPOGVG3GPHH27OMPKR434BSX --invoke-contract-args AAAAAUX05neMv38MV/DiJGmlSlHA3w+b3GqbZnnPr7mPVHm+AAAABmRlcGxveQAAAAAABgAAAA4AAAADYWNjAAAAAAEAAAAOAAAAGWFjY190ZXN0X25ld192ZXJzaW9uX21vcmUAAAAAAAASAAAAAAAAAAAebE8Ewg9uzvHRAl+UmvP0kixsyp238kkz0zOy+91FeQAAAAEAAAAB --build-only
AAAAAgAAAAAebE8Ewg9uzvHRAl+UmvP0kixsyp238kkz0zOy+91FeQAAAGQABNeOAAAAIwAAAAAAAAAAAAAAAQAAAAAAAAAYAAAAAAAAAAFF9OZ3jL9/DFfw4iRppUpRwN8Pm9xqm2Z5z6+5j1R5vgAAAAZkZXBsb3kAAAAAAAYAAAAOAAAAA2FjYwAAAAABAAAADgAAABlhY2NfdGVzdF9uZXdfdmVyc2lvbl9tb3JlAAAAAAAAEgAAAAAAAAAAHmxPBMIPbs7x0QJflJrz9JIsbMqdt/JJM9MzsvvdRXkAAAABAAAAAQAAAAAAAAAAAAAAAA==
  1. > cargo run contract invoke --source-account alice --id CBC7JZTXRS7X6DCX6DRCI2NFJJI4BXYPTPOGVG3GPHH27OMPKR434BSX --build-only -- deploy --wasm-name acc --contract-name acc_test_new_version_more --admin GAPGYTYEYIHW5TXR2EBF7FE26P2JELDMZKO3P4SJGPJTHMX33VCXSKV7
AAAAAgAAAAAebE8Ewg9uzvHRAl+UmvP0kixsyp238kkz0zOy+91FeQAAAGQABNeOAAAAIwAAAAAAAAAAAAAAAQAAAAAAAAAYAAAAAAAAAAFF9OZ3jL9/DFfw4iRppUpRwN8Pm9xqm2Z5z6+5j1R5vgAAAAZkZXBsb3kAAAAAAAYAAAAOAAAAA2FjYwAAAAABAAAADgAAABlhY2NfdGVzdF9uZXdfdmVyc2lvbl9tb3JlAAAAAAAAEgAAAAAAAAAAHmxPBMIPbs7x0QJflJrz9JIsbMqdt/JJM9MzsvvdRXkAAAABAAAAAQAAAAAAAAAAAAAAAA==
  1. Verify output is identical
image

Copilot AI review requested due to automatic review settings March 23, 2026 04:22
@github-project-automation github-project-automation bot moved this to Backlog (Not Ready) in DevX Mar 23, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an alternate contract invocation flow that accepts pre-encoded base64 XDR arguments (string or file input), and removes a local whitespace-stripping reader now that stellar-xdr provides SkipWhitespace.

Changes:

  • Use stellar_xdr::curr::SkipWhitespace directly (remove local SkipWhitespace shim).
  • Add --invoke-contract-args to stellar contract invoke and make the existing “slop” args optional/alternative.
  • Implement XDR decoding helpers in contract arg parsing to build invoke parameters from base64 XDR.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
cmd/soroban-cli/src/commands/tx/xdr.rs Switches to upstream SkipWhitespace and removes local implementation.
cmd/soroban-cli/src/commands/contract/invoke.rs Adds --invoke-contract-args and routes invocation parameter building accordingly.
cmd/soroban-cli/src/commands/contract/arg_parsing.rs Adds XDR decoding path for InvokeContractArgs and plumbing to build host function parameters from it.

let mut lim = Limited::new(SkipWhitespace::new(read), Limits::none());
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.
@@ -55,9 +56,16 @@ 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.
Comment on lines +59 to +68
#[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>>,
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.
Comment on lines +166 to +175
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());
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.
@chadoh
Copy link
Copy Markdown
Contributor

chadoh commented Mar 27, 2026

Should these be the same contract ID?

contract ID in encoded 'function' object vs contract id in 'contract invoke' command

I find it a little bit odd/unfortunate that we need to encode the entire function object into XDR, rather than just the args array. For one, the args are the only hard thing to do in the "replay transactions from history" context, not the contract ID and function name. For another, the contract ID is duplicated (I think?) between the still-existing --id arg and the encoded parameters.

Ideally, I would want an interface like:

stellar contract invoke --id … -s … -- deploy --args-xdr …

Which would accept only the args portion encoded as XDR.

I understand that this creates the possibility of clobbering a contract-defined function if it happens to have the name args_xdr. And maybe there's other reasons to not do this, or that reorganizing this PR in such a way would be too difficult to be worthwhile.

Alternatively, if we just rename invoke-contract-args, then I would already like the current implementation more. And if we can make --id incompatible with this new arg, to avoid contradictory input, that seems nice. So the interface might become something like:

stellar contract invoke -s alice --function-xdr …

Hmmm. That might be nice because it specifies that you need to encode a function as XDR. But maybe another name would be better?

Backing up: this deviating enough from the other invoke functionality that we should make a new subcommand?

stellar contract invoke-raw -s alice --xdr AAAAAUX05…

Or maybe we add this to the tx subcommand. It looks like the current tx functionality is still a little higher-level than we ideally want; maybe we add a new tx from-xdr command:

stellar tx from-xdr AAAAAUX05… | stellar tx sign --sign-with-key alice | stellar tx send

Would we need to encode a larger parent object, or would just the function object shown in your example be enough?

Either way, I think I like this approach best. The tx interface is currently quite small, so we avoid cluttering the more frequently-used and larger contract interface further with something like invoke-raw, and we avoid further complicating the invoke behavior. And tx feels lower-level, which is a good match for this use-case.

This might also effectively work around the bug in #2459, which is blocking the contract invoke approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

3 participants