Conversation
There was a problem hiding this comment.
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::SkipWhitespacedirectly (remove localSkipWhitespaceshim). - Add
--invoke-contract-argstostellar contract invokeand 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 }) | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| #[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:?}"), | |
| } | |
| } | |
| } |
| @@ -55,9 +56,16 @@ pub struct Cmd { | |||
| #[arg(long, env = "STELLAR_INVOKE_VIEW")] | |||
| pub is_view: bool, | |||
|
|
|||
There was a problem hiding this comment.
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.
| /// 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. |
| #[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>>, |
There was a problem hiding this comment.
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.
| 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()); |
There was a problem hiding this comment.
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.
| 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()); |
What
Add ability to call
contract invokewith base64 encoded InvokeContractArgsWhy
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:
> cargo run contract invoke --source-account alice --id CBC7JZTXRS7X6DCX6DRCI2NFJJI4BXYPTPOGVG3GPHH27OMPKR434BSX --invoke-contract-args AAAAAUX05neMv38MV/DiJGmlSlHA3w+b3GqbZnnPr7mPVHm+AAAABmRlcGxveQAAAAAABgAAAA4AAAADYWNjAAAAAAEAAAAOAAAAGWFjY190ZXN0X25ld192ZXJzaW9uX21vcmUAAAAAAAASAAAAAAAAAAAebE8Ewg9uzvHRAl+UmvP0kixsyp238kkz0zOy+91FeQAAAAEAAAAB --build-only> cargo run contract invoke --source-account alice --id CBC7JZTXRS7X6DCX6DRCI2NFJJI4BXYPTPOGVG3GPHH27OMPKR434BSX --build-only -- deploy --wasm-name acc --contract-name acc_test_new_version_more --admin GAPGYTYEYIHW5TXR2EBF7FE26P2JELDMZKO3P4SJGPJTHMX33VCXSKV7