diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index e197e0238..30e4ff62a 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1877,6 +1877,7 @@ Create a new transaction - `revoke-sponsorship` — Revoke sponsorship of a ledger entry or signer - `set-options` — Set account options like flags, signers, and home domain - `set-trustline-flags` — Configure authorization and trustline flags for an asset +- `invoke` — Invoke a smart contract using a given InvokeHostFunctionOp XDR ## `stellar tx new account-merge` @@ -2624,6 +2625,36 @@ Configure authorization and trustline flags for an asset - `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server - `-n`, `--network ` — Name of network to use from config +## `stellar tx new invoke` + +Invoke a smart contract using a given InvokeHostFunctionOp XDR + +**Usage:** `stellar tx new invoke [OPTIONS] --source-account --xdr ` + +###### **Options:** + +- `-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` +- `--sign-with-lab` — Sign with https://lab.stellar.org +- `--sign-with-ledger` — Sign with a ledger wallet +- `--fee ` — ⚠️ Deprecated, use `--inclusion-fee`. Fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm +- `--inclusion-fee ` — Maximum fee amount for transaction inclusion, in stroops. 1 stroop = 0.0000001 xlm. Defaults to 100 if no arg, env, or config value is provided +- `--build-only` — Build the transaction and only write the base64 xdr to stdout +- `--xdr ` — Base-64 InvokeContractArgs envelope XDR or file containing XDR to decode + +###### **Options (Global):** + +- `--global` — ⚠️ Deprecated: global config is always on +- `--config-dir ` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings + +###### **Options (RPC):** + +- `--rpc-url ` — RPC server endpoint +- `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times +- `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +- `-n`, `--network ` — Name of network to use from config + ## `stellar tx operation` Manipulate the operations in a transaction, including adding new operations diff --git a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs index 86297b456..9b7464bdc 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,12 @@ 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, InvokeHostFunctionOp, Limited, Limits, ReadXdr, SkipWhitespace, +}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -77,6 +81,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 +134,20 @@ async fn build_host_function_parameters_with_filter( Ok((function, spec, invoke_args, signers)) } +pub fn invoke_host_function_op_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()); + InvokeHostFunctionOp::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/tx/args.rs b/cmd/soroban-cli/src/commands/tx/args.rs index 2b4c766c3..62648ea16 100644 --- a/cmd/soroban-cli/src/commands/tx/args.rs +++ b/cmd/soroban-cli/src/commands/tx/args.rs @@ -1,3 +1,4 @@ +use crate::commands::contract::arg_parsing; use crate::{ commands::{global, txn_result::TxnEnvelopeResult}, config::{ @@ -50,6 +51,8 @@ pub enum Error { InvalidPoolId(String), #[error("invalid hex for {name}: {hex}")] InvalidHex { name: String, hex: String }, + #[error(transparent)] + ArgParsing(#[from] arg_parsing::Error), } impl Args { diff --git a/cmd/soroban-cli/src/commands/tx/help.rs b/cmd/soroban-cli/src/commands/tx/help.rs index 21f760918..e0301aead 100644 --- a/cmd/soroban-cli/src/commands/tx/help.rs +++ b/cmd/soroban-cli/src/commands/tx/help.rs @@ -24,3 +24,4 @@ pub const BEGIN_SPONSORING_FUTURE_RESERVES: &str = "Begin sponsoring future reserves for another account"; pub const END_SPONSORING_FUTURE_RESERVES: &str = "End sponsoring future reserves"; pub const REVOKE_SPONSORSHIP: &str = "Revoke sponsorship of a ledger entry or signer"; +pub const INVOKE: &str = "Invoke a smart contract using a given InvokeHostFunctionOp XDR"; diff --git a/cmd/soroban-cli/src/commands/tx/new/invoke.rs b/cmd/soroban-cli/src/commands/tx/new/invoke.rs new file mode 100644 index 000000000..89dc903e8 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/invoke.rs @@ -0,0 +1,31 @@ +use crate::commands::contract::arg_parsing::invoke_host_function_op_from_input; +use crate::{commands::tx, xdr}; +use clap::Parser; +use std::ffi::OsString; +use stellar_xdr::curr::OperationBody; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + #[clap(flatten)] + pub op: Args, +} + +#[derive(Debug, clap::Args, Clone)] +#[allow(clippy::struct_excessive_bools, clippy::doc_markdown)] +pub struct Args { + /// Base-64 InvokeContractArgs envelope XDR or file containing XDR to decode. + #[arg(long)] + pub xdr: OsString, +} + +impl TryFrom<&Cmd> for xdr::OperationBody { + type Error = tx::args::Error; + fn try_from(cmd: &Cmd) -> Result { + let parameters = invoke_host_function_op_from_input(&cmd.op.xdr)?; + + Ok(OperationBody::InvokeHostFunction(parameters)) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/mod.rs b/cmd/soroban-cli/src/commands/tx/new/mod.rs index 7126bd276..418179b7f 100644 --- a/cmd/soroban-cli/src/commands/tx/new/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/new/mod.rs @@ -14,6 +14,7 @@ pub mod create_account; pub mod create_claimable_balance; pub mod create_passive_sell_offer; pub mod end_sponsoring_future_reserves; +pub mod invoke; pub mod liquidity_pool_deposit; pub mod liquidity_pool_withdraw; pub mod manage_buy_offer; @@ -73,6 +74,8 @@ pub enum Cmd { SetOptions(set_options::Cmd), #[command(about = super::help::SET_TRUSTLINE_FLAGS)] SetTrustlineFlags(set_trustline_flags::Cmd), + #[command(about = super::help::INVOKE)] + Invoke(invoke::Cmd), } #[derive(thiserror::Error, Debug)] @@ -107,6 +110,7 @@ impl TryFrom<&Cmd> for OperationBody { Cmd::RevokeSponsorship(cmd) => cmd.try_into()?, Cmd::SetOptions(cmd) => cmd.try_into()?, Cmd::SetTrustlineFlags(cmd) => cmd.try_into()?, + Cmd::Invoke(cmd) => cmd.try_into()?, }) } } @@ -139,6 +143,7 @@ impl Cmd { Cmd::RevokeSponsorship(cmd) => cmd.tx.handle_and_print(op, global_args).await, Cmd::SetOptions(cmd) => cmd.tx.handle_and_print(op, global_args).await, Cmd::SetTrustlineFlags(cmd) => cmd.tx.handle_and_print(op, global_args).await, + Cmd::Invoke(cmd) => cmd.tx.handle_and_print(op, global_args).await, }?; Ok(()) } 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);