diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index e197e0238..9a964eb72 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -895,6 +895,7 @@ stellar contract invoke ... -- --help - `--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 ` — (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 ` [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 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 ` — 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` diff --git a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs index 86297b456..9c93a03c7 100644 --- a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs +++ b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs @@ -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}; @@ -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 { @@ -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); @@ -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 { + let spec = Spec(Some(spec_entries.to_vec())); + let invoke_args = invoke_contract_args_from_input(string_xdr)?; + let mut signers = Vec::::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 { + 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()); + InvokeContractArgs::read_xdr_base64_to_end(&mut lim).map_err(|e| CannotParseXDR { error: e }) +} + fn build_clap_command(spec: &Spec, filter_constructor: bool) -> Result { let mut cmd = clap::Command::new(running_cmd()) .no_binary_name(true) diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index bebba454a..20af1e7db 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -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; @@ -55,9 +56,18 @@ pub struct Cmd { #[arg(long, env = "STELLAR_INVOKE_VIEW")] pub is_view: bool, + /// (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, + /// Function name as subcommand, then arguments for that function as `--arg-name value` - #[arg(last = true, id = "CONTRACT_FN_AND_ARGS")] - pub slop: Vec, + #[arg( + last = true, + id = "CONTRACT_FN_AND_ARGS", + conflicts_with = "invoke_contract_args" + )] + pub slop: Option>, #[command(flatten)] pub config: config::Args, @@ -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()?; @@ -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; diff --git a/cmd/soroban-cli/src/commands/tx/xdr.rs b/cmd/soroban-cli/src/commands/tx/xdr.rs index f9d0ef0c6..509aa4c31 100644 --- a/cmd/soroban-cli/src/commands/tx/xdr.rs +++ b/cmd/soroban-cli/src/commands/tx/xdr.rs @@ -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 { @@ -41,34 +41,6 @@ pub fn tx_envelope_from_input(input: &Option) -> Result { - pub inner: R, -} - -impl SkipWhitespace { - pub fn new(inner: R) -> Self { - SkipWhitespace { inner } - } -} - -impl Read for SkipWhitespace { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - 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 { let TransactionEnvelope::Tx(TransactionV1Envelope { tx, .. }) = tx_env else { return Err(Error::OnlyTransactionV1Supported);