From 71ecb561be558635118f8406ebab5046b8290aaf Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 2 May 2024 11:49:42 -0400 Subject: [PATCH] feat: --no-build command allows returning built transaction --sim-only for assembling transactions --- cmd/crates/soroban-test/src/lib.rs | 35 ++++--- .../tests/it/integration/hello_world.rs | 24 ++++- .../src/commands/contract/deploy/asset.rs | 15 ++- .../src/commands/contract/deploy/wasm.rs | 24 +++-- .../src/commands/contract/extend.rs | 20 ++-- .../src/commands/contract/fetch.rs | 14 ++- .../src/commands/contract/install.rs | 16 ++- .../src/commands/contract/invoke.rs | 99 ++++++++++++------- cmd/soroban-cli/src/commands/contract/read.rs | 14 ++- .../src/commands/contract/restore.rs | 26 +++-- cmd/soroban-cli/src/commands/events.rs | 33 ++++--- cmd/soroban-cli/src/commands/mod.rs | 4 +- cmd/soroban-cli/src/commands/txn_result.rs | 56 +++++++++++ cmd/soroban-cli/src/fee.rs | 25 ++++- cmd/soroban-cli/src/lib.rs | 2 + docs/soroban-cli-full-docs.md | 51 ++++++++++ 16 files changed, 353 insertions(+), 105 deletions(-) create mode 100644 cmd/soroban-cli/src/commands/txn_result.rs diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index e4d7410ce4..835d2066b3 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -236,22 +236,25 @@ impl TestEnv { }, hd_path: None, }; - cmd.run_against_rpc_server( - Some(&global::Args { - locator: config::locator::Args { - global: false, - config_dir, - }, - filter_logs: Vec::default(), - quiet: false, - verbose: false, - very_verbose: false, - list: false, - no_cache: false, - }), - Some(&config), - ) - .await + Ok(cmd + .run_against_rpc_server( + Some(&global::Args { + locator: config::locator::Args { + global: false, + config_dir, + }, + filter_logs: Vec::default(), + quiet: false, + verbose: false, + very_verbose: false, + list: false, + no_cache: false, + }), + Some(&config), + ) + .await? + .res() + .unwrap()) } /// Reference to current directory of the `TestEnv`. diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index be03afa8d5..758c6fce02 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -11,6 +11,18 @@ use crate::integration::util::extend_contract; use super::util::{deploy_hello, extend, HELLO_WORLD}; #[allow(clippy::too_many_lines)] +#[tokio::test] +async fn invoke_view_with_non_existent_source_account() { + let sandbox = &TestEnv::new(); + let id = deploy_hello(sandbox).await; + let world = "world"; + let mut cmd = hello_world_cmd(&id, world); + cmd.config.source_account = String::new(); + cmd.is_view = true; + let res = sandbox.run_cmd_with(cmd, "test").await.unwrap(); + assert_eq!(res, format!(r#"["Hello",{world:?}]"#)); +} + #[tokio::test] async fn invoke() { let sandbox = &TestEnv::new(); @@ -140,12 +152,16 @@ fn invoke_hello_world(sandbox: &TestEnv, id: &str) { .success(); } -async fn invoke_hello_world_with_lib(e: &TestEnv, id: &str) { - let cmd = contract::invoke::Cmd { +fn hello_world_cmd(id: &str, arg: &str) -> contract::invoke::Cmd { + contract::invoke::Cmd { contract_id: id.to_string(), - slop: vec!["hello".into(), "--world=world".into()], + slop: vec!["hello".into(), format!("--world={arg}").into()], ..Default::default() - }; + } +} + +async fn invoke_hello_world_with_lib(e: &TestEnv, id: &str) { + let cmd = hello_world_cmd(id, "world"); let res = e.run_cmd_with(cmd, "test").await.unwrap(); assert_eq!(res, r#"["Hello","world"]"#); } diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 41e7367dce..65557eda9f 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -14,7 +14,9 @@ use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; use crate::{ commands::{ config::{self, data}, - global, network, NetworkRunnable, + global, network, + txn_result::TxnResult, + NetworkRunnable, }, rpc::{Client, Error as SorobanRpcError}, utils::{contract_id_hash_from_asset, parsing::parse_asset}, @@ -80,7 +82,7 @@ impl NetworkRunnable for Cmd { &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); // Parse asset let asset = parse_asset(&self.asset)?; @@ -108,8 +110,11 @@ impl NetworkRunnable for Cmd { network_passphrase, &key, )?; + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx)?); + } let txn = client.create_assembled_transaction(&tx).await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = self.fee.apply_to_assembled_txn(txn)?; let get_txn_resp = client .send_assembled_transaction(txn, &key, &[], network_passphrase, None, None) .await? @@ -118,7 +123,9 @@ impl NetworkRunnable for Cmd { data::write(get_txn_resp, &network.rpc_uri()?)?; } - Ok(stellar_strkey::Contract(contract_id.0).to_string()) + Ok(TxnResult::Xdr( + stellar_strkey::Contract(contract_id.0).to_string(), + )) } } diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 40369b2301..7a2b929d41 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -17,7 +17,9 @@ use soroban_env_host::{ use crate::commands::{ config::data, contract::{self, id::wasm::get_contract_id}, - global, network, NetworkRunnable, + global, network, + txn_result::{self, TxnResult}, + NetworkRunnable, }; use crate::{ commands::{config, contract::install, HEADING_RPC}, @@ -93,6 +95,8 @@ pub enum Error { #[error(transparent)] WasmId(#[from] contract::id::wasm::Error), #[error(transparent)] + TxnResult(#[from] txn_result::Error), + #[error(transparent)] Data(#[from] data::Error), #[error(transparent)] Network(#[from] network::Error), @@ -115,18 +119,20 @@ impl NetworkRunnable for Cmd { &self, global_args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let wasm_hash = if let Some(wasm) = &self.wasm { + let mut fee = self.fee.clone(); + fee.build_only = false; let hash = install::Cmd { wasm: wasm::Args { wasm: wasm.clone() }, config: config.clone(), - fee: self.fee.clone(), + fee, ignore_checks: self.ignore_checks, } .run_against_rpc_server(global_args, Some(config)) .await?; - hex::encode(hash) + hex::encode(hash.try_res()?) } else { self.wasm_hash .as_ref() @@ -169,8 +175,12 @@ impl NetworkRunnable for Cmd { salt, &key, )?; + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&txn)?); + } + let txn = client.create_assembled_transaction(&txn).await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = self.fee.apply_to_assembled_txn(txn)?; let get_txn_resp = client .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) .await? @@ -178,7 +188,9 @@ impl NetworkRunnable for Cmd { if global_args.map_or(true, |a| !a.no_cache) { data::write(get_txn_resp, &network.rpc_uri()?)?; } - Ok(stellar_strkey::Contract(contract_id.0).to_string()) + Ok(TxnResult::Res( + stellar_strkey::Contract(contract_id.0).to_string(), + )) } } diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index ab7834959b..cebddd0c03 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -11,7 +11,9 @@ use soroban_env_host::xdr::{ use crate::{ commands::{ config::{self, data}, - global, network, NetworkRunnable, + global, network, + txn_result::TxnResult, + NetworkRunnable, }, key, rpc::{self, Client}, @@ -87,7 +89,11 @@ pub enum Error { impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let ttl_ledger = self.run_against_rpc_server(None, None).await?; + let res = self.run_against_rpc_server(None, None).await?; + let TxnResult::Res(ttl_ledger) = &res else { + println!("{}", res.xdr().unwrap()); + return Ok(()); + }; if self.ttl_ledger_only { println!("{ttl_ledger}"); } else { @@ -117,7 +123,7 @@ impl NetworkRunnable for Cmd { &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Self::Error> { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); @@ -161,7 +167,9 @@ impl NetworkRunnable for Cmd { resource_fee: 0, }), }; - + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx)?); + } let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; @@ -194,7 +202,7 @@ impl NetworkRunnable for Cmd { let entry = client.get_full_ledger_entries(&keys).await?; let extension = entry.entries[0].live_until_ledger_seq; if entry.latest_ledger + i64::from(extend_to) < i64::from(extension) { - return Ok(extension); + return Ok(TxnResult::Res(extension)); } } @@ -209,7 +217,7 @@ impl NetworkRunnable for Cmd { }), .. }), - ) => Ok(*live_until_ledger_seq), + ) => Ok(TxnResult::Res(*live_until_ledger_seq)), _ => Err(Error::LedgerEntryNotFound), } } diff --git a/cmd/soroban-cli/src/commands/contract/fetch.rs b/cmd/soroban-cli/src/commands/contract/fetch.rs index eefb1b4b89..5b223e71ad 100644 --- a/cmd/soroban-cli/src/commands/contract/fetch.rs +++ b/cmd/soroban-cli/src/commands/contract/fetch.rs @@ -21,6 +21,7 @@ use stellar_strkey::DecodeError; use super::super::config::{self, locator}; use crate::commands::network::{self, Network}; +use crate::commands::txn_result::TxnResult; use crate::commands::{global, NetworkRunnable}; use crate::{ rpc::{self, Client}, @@ -116,7 +117,14 @@ impl Cmd { } pub async fn get_bytes(&self) -> Result, Error> { - self.run_against_rpc_server(None, None).await + // This is safe because fetch doesn't create a transaction + unsafe { + Ok(self + .run_against_rpc_server(None, None) + .await? + .res() + .unwrap_unchecked()) + } } pub fn network(&self) -> Result { @@ -137,7 +145,7 @@ impl NetworkRunnable for Cmd { &self, _args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result, Error> { + ) -> Result>, Error> { let network = config.map_or_else(|| self.network(), |c| Ok(c.get_network()?))?; tracing::trace!(?network); let contract_id = self.contract_id()?; @@ -146,7 +154,7 @@ impl NetworkRunnable for Cmd { .verify_network_passphrase(Some(&network.network_passphrase)) .await?; // async closures are not yet stable - Ok(client.get_remote_wasm(&contract_id).await?) + Ok(TxnResult::Res(client.get_remote_wasm(&contract_id).await?)) } } pub fn get_contract_wasm_from_storage( diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 8763fd7e67..9166c48cbf 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -12,6 +12,7 @@ use soroban_env_host::xdr::{ use super::restore; use crate::commands::network; +use crate::commands::txn_result::TxnResult; use crate::commands::{config::data, global, NetworkRunnable}; use crate::key; use crate::rpc::{self, Client}; @@ -72,7 +73,10 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let res_str = hex::encode(self.run_against_rpc_server(None, None).await?); + let res_str = match self.run_against_rpc_server(None, None).await? { + TxnResult::Xdr(xdr) => xdr, + TxnResult::Res(hash) => hex::encode(hash), + }; println!("{res_str}"); Ok(()) } @@ -86,7 +90,7 @@ impl NetworkRunnable for Cmd { &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let contract = self.wasm.read()?; let network = config.get_network()?; @@ -125,6 +129,9 @@ impl NetworkRunnable for Cmd { let (tx_without_preflight, hash) = build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &key)?; + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx_without_preflight)?); + } let code_key = xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() }); let contract_data = client.get_ledger_entries(&[code_key]).await?; @@ -151,11 +158,10 @@ impl NetworkRunnable for Cmd { } } } - let txn = client .create_assembled_transaction(&tx_without_preflight) .await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = self.fee.apply_to_assembled_txn(txn)?; let txn_resp = client .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) .await?; @@ -189,7 +195,7 @@ impl NetworkRunnable for Cmd { if args.map_or(true, |a| !a.no_cache) { data::write_spec(&hash.to_string(), &wasm_spec.spec)?; } - Ok(hash) + Ok(TxnResult::Res(hash)) } } diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index d40f9c46fc..00d7383d7d 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -12,15 +12,16 @@ use heck::ToKebabCase; use soroban_env_host::{ xdr::{ - self, ContractDataEntry, Error as XdrError, Hash, HostFunction, InvokeContractArgs, - InvokeHostFunctionOp, LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, - OperationBody, Preconditions, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, - ScVal, ScVec, SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, + self, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, LedgerEntryData, + LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, Preconditions, PublicKey, + ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, SequenceNumber, + SorobanAuthorizationEntry, SorobanResources, String32, StringM, Transaction, TransactionExt, Uint256, VecM, }, HostError, }; +use soroban_sdk::xdr::{AccountEntry, AccountEntryExt, AccountId, ContractDataEntry, DiagnosticEvent, Thresholds}; use soroban_spec::read::FromWasmError; use stellar_strkey::DecodeError; @@ -28,6 +29,7 @@ use super::super::{ config::{self, locator}, events, }; +use crate::commands::txn_result::TxnResult; use crate::commands::NetworkRunnable; use crate::{ commands::{config::data, global, network}, @@ -80,7 +82,7 @@ pub enum Error { error: soroban_spec_tools::Error, }, #[error("cannot add contract to ledger entries: {0}")] - CannotAddContractToLedgerEntries(XdrError), + CannotAddContractToLedgerEntries(xdr::Error), #[error(transparent)] // TODO: the Display impl of host errors is pretty user-unfriendly // (it just calls Debug). I think we can do better than that @@ -109,7 +111,7 @@ pub enum Error { error: soroban_spec_tools::Error, }, #[error(transparent)] - Xdr(#[from] XdrError), + Xdr(#[from] xdr::Error), #[error("error parsing int: {0}")] ParseIntError(#[from] ParseIntError), #[error(transparent)] @@ -169,6 +171,7 @@ impl Cmd { &self, contract_id: [u8; 32], spec_entries: &[ScSpecEntry], + config: &config::Args, ) -> Result<(String, Spec, InvokeContractArgs, Vec), Error> { let spec = Spec(Some(spec_entries.to_vec())); let mut cmd = clap::Command::new(self.contract_id.clone()) @@ -201,7 +204,7 @@ impl Cmd { let cmd = crate::commands::keys::address::Cmd { name: s.clone(), hd_path: Some(0), - locator: self.config.locator.clone(), + locator: config.locator.clone(), }; if let Ok(address) = cmd.public_key() { s = address.to_string(); @@ -272,7 +275,7 @@ impl Cmd { Ok(()) } - pub async fn invoke(&self, global_args: &global::Args) -> Result { + pub async fn invoke(&self, global_args: &global::Args) -> Result, Error> { self.run_against_rpc_server(Some(global_args), None).await } @@ -309,7 +312,7 @@ impl NetworkRunnable for Cmd { &self, global_args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); @@ -317,19 +320,24 @@ impl NetworkRunnable for Cmd { let spec_entries = self.spec_entries()?; if let Some(spec_entries) = &spec_entries { // For testing wasm arg parsing - let _ = self.build_host_function_parameters(contract_id, spec_entries)?; + let _ = self.build_host_function_parameters(contract_id, spec_entries, config)?; } let client = rpc::Client::new(&network.rpc_url)?; - client - .verify_network_passphrase(Some(&network.network_passphrase)) - .await?; - let key = config.key_pair()?; - - // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - let account_details = client.get_account(&public_strkey).await?; + let account_details = if self.is_view { + default_account_entry() + } else { + client + .verify_network_passphrase(Some(&network.network_passphrase)) + .await?; + let key = config.key_pair()?; + + // Get the account sequence number + let public_strkey = + stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); + client.get_account(&public_strkey).await? + }; let sequence: i64 = account_details.seq_num.into(); + let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) = account_details.account_id; let r = client.get_contract_data(&contract_id).await?; tracing::trace!("{r:?}"); @@ -361,15 +369,18 @@ impl NetworkRunnable for Cmd { // Get the ledger footprint let (function, spec, host_function_params, signers) = - self.build_host_function_parameters(contract_id, &spec_entries)?; + self.build_host_function_parameters(contract_id, &spec_entries, config)?; let tx = build_invoke_contract_tx( host_function_params.clone(), sequence + 1, self.fee.fee, - &key, + account_id, )?; + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx)?); + } let txn = client.create_assembled_transaction(&tx).await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = self.fee.apply_to_assembled_txn(txn)?; let sim_res = txn.sim_response(); if global_args.map_or(true, |a| !a.no_cache) { data::write(sim_res.clone().into(), &network.rpc_uri()?)?; @@ -386,7 +397,7 @@ impl NetworkRunnable for Cmd { let res = client .send_assembled_transaction( txn, - &key, + &config.key_pair()?, &signers, &network.network_passphrase, Some(log_events), @@ -404,10 +415,27 @@ impl NetworkRunnable for Cmd { } } +const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))); + +fn default_account_entry() -> AccountEntry { + AccountEntry { + account_id: DEFAULT_ACCOUNT_ID, + balance: 0, + seq_num: SequenceNumber(0), + num_sub_entries: 0, + inflation_dest: None, + flags: 0, + home_domain: String32::from(unsafe { StringM::<32>::from_str("TEST").unwrap_unchecked() }), + thresholds: Thresholds([0; 4]), + signers: unsafe { [].try_into().unwrap_unchecked() }, + ext: AccountEntryExt::V0, + } +} + fn log_events( footprint: &LedgerFootprint, auth: &[VecM], - events: &[xdr::DiagnosticEvent], + events: &[DiagnosticEvent], ) { crate::log::auth(auth); crate::log::diagnostic_events(events, tracing::Level::TRACE); @@ -418,7 +446,11 @@ fn log_resources(resources: &SorobanResources) { crate::log::cost(resources); } -pub fn output_to_string(spec: &Spec, res: &ScVal, function: &str) -> Result { +pub fn output_to_string( + spec: &Spec, + res: &ScVal, + function: &str, +) -> Result, Error> { let mut res_str = String::new(); if let Some(output) = spec.find_function(function)?.outputs.first() { res_str = spec @@ -429,14 +461,14 @@ pub fn output_to_string(spec: &Spec, res: &ScVal, function: &str) -> Result Result { let op = Operation { source_account: None, @@ -446,7 +478,7 @@ fn build_invoke_contract_tx( }), }; Ok(Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.verifying_key().to_bytes())), + source_account: MuxedAccount::Ed25519(source_account_id), fee, seq_num: SequenceNumber(sequence), cond: Preconditions::None, @@ -506,16 +538,15 @@ fn build_custom_cmd(name: &str, spec: &Spec) -> Result { // Set up special-case arg rules arg = match type_ { - xdr::ScSpecTypeDef::Bool => arg + ScSpecTypeDef::Bool => arg .num_args(0..1) .default_missing_value("true") .default_value("false") .num_args(0..=1), - xdr::ScSpecTypeDef::Option(_val) => arg.required(false), - xdr::ScSpecTypeDef::I256 - | xdr::ScSpecTypeDef::I128 - | xdr::ScSpecTypeDef::I64 - | xdr::ScSpecTypeDef::I32 => arg.allow_hyphen_values(true), + ScSpecTypeDef::Option(_val) => arg.required(false), + ScSpecTypeDef::I256 | ScSpecTypeDef::I128 | ScSpecTypeDef::I64 | ScSpecTypeDef::I32 => { + arg.allow_hyphen_values(true) + } _ => arg, }; diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index a7b1d07a8b..1d23e5c6ab 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -14,7 +14,7 @@ use soroban_env_host::{ use soroban_sdk::xdr::Limits; use crate::{ - commands::{config, global, NetworkRunnable}, + commands::{config, global, txn_result::TxnResult, NetworkRunnable}, key, rpc::{self, Client, FullLedgerEntries, FullLedgerEntry}, }; @@ -91,7 +91,13 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let entries = self.run_against_rpc_server(None, None).await?; + let entries = match self.run_against_rpc_server(None, None).await? { + TxnResult::Res(res) => res, + TxnResult::Xdr(xdr) => { + println!("{xdr}"); + return Ok(()); + } + }; self.output_entries(&entries) } @@ -178,12 +184,12 @@ impl NetworkRunnable for Cmd { &self, _: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); let client = Client::new(&network.rpc_url)?; let keys = self.key.parse_keys()?; - Ok(client.get_full_ledger_entries(&keys).await?) + Ok(TxnResult::Res(client.get_full_ledger_entries(&keys).await?)) } } diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 8b5921a1a6..6fc8eb17ff 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -13,7 +13,9 @@ use crate::{ commands::{ config::{self, data, locator}, contract::extend, - global, network, NetworkRunnable, + global, network, + txn_result::{self, TxnResult}, + NetworkRunnable, }, key, rpc::{self, Client}, @@ -87,13 +89,21 @@ pub enum Error { Data(#[from] data::Error), #[error(transparent)] Network(#[from] network::Error), + + #[error(transparent)] + TxnResult(#[from] txn_result::Error), } impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let expiration_ledger_seq = self.run_against_rpc_server(None, None).await?; - + let expiration_ledger_seq = match self.run_against_rpc_server(None, None).await? { + TxnResult::Res(res) => res, + TxnResult::Xdr(xdr) => { + println!("{xdr}"); + return Ok(()); + } + }; if let Some(ledgers_to_extend) = self.ledgers_to_extend { extend::Cmd { key: self.key.clone(), @@ -121,7 +131,7 @@ impl NetworkRunnable for Cmd { &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); @@ -162,7 +172,9 @@ impl NetworkRunnable for Cmd { resource_fee: 0, }), }; - + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx)?); + } let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; @@ -198,7 +210,9 @@ impl NetworkRunnable for Cmd { operations[0].changes.len() ); } - parse_operations(operations).ok_or(Error::MissingOperationResult) + Ok(TxnResult::Res( + parse_operations(operations).ok_or(Error::MissingOperationResult)?, + )) } } diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index 42145f5bfd..f1707f3499 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -5,7 +5,9 @@ use soroban_env_host::xdr::{self, Limits, ReadXdr}; use super::{ config::{self, locator}, - global, network, NetworkRunnable, + global, network, + txn_result::TxnResult, + NetworkRunnable, }; use crate::{rpc, utils}; @@ -124,6 +126,8 @@ pub enum Error { Locator(#[from] locator::Error), #[error(transparent)] Config(#[from] config::Error), + #[error(transparent)] + TxnResult(#[from] super::txn_result::Error), } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] @@ -167,7 +171,8 @@ impl Cmd { })?; } - let response = self.run_against_rpc_server(None, None).await?; + let txn_res = self.run_against_rpc_server(None, None).await?; + let response = txn_res.try_res()?; for event in &response.events { match self.output { @@ -214,7 +219,7 @@ impl NetworkRunnable for Cmd { &self, _args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let start = self.start()?; let network = if let Some(config) = config { Ok(config.get_network()?) @@ -226,15 +231,17 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - client - .get_events( - start, - Some(self.event_type), - &self.contract_ids, - &self.topic_filters, - Some(self.count), - ) - .await - .map_err(Error::Rpc) + Ok(TxnResult::Res( + client + .get_events( + start, + Some(self.event_type), + &self.contract_ids, + &self.topic_filters, + Some(self.count), + ) + .await + .map_err(Error::Rpc)?, + )) } } diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 4bb5dcb53b..f904c465f6 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -14,6 +14,8 @@ pub mod network; pub mod plugin; pub mod version; +pub mod txn_result; + pub const HEADING_RPC: &str = "Options (RPC)"; const ABOUT: &str = "Build, deploy, & interact with contracts; set identities to sign with; configure networks; generate keys; and more. @@ -168,5 +170,5 @@ pub trait NetworkRunnable { &self, global_args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result; + ) -> Result, Self::Error>; } diff --git a/cmd/soroban-cli/src/commands/txn_result.rs b/cmd/soroban-cli/src/commands/txn_result.rs new file mode 100644 index 0000000000..f69f549bef --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn_result.rs @@ -0,0 +1,56 @@ +use std::fmt::{Display, Formatter}; + +use soroban_sdk::xdr::{self, Limits, WriteXdr}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Expect xdr string")] + XdrStringExpected, + #[error("Expect result")] + ResultExpected, +} + +pub enum TxnResult { + Xdr(String), + Res(T), +} + +impl TxnResult { + pub fn from_xdr(res: &impl WriteXdr) -> Result { + Ok(TxnResult::Xdr(res.to_xdr_base64(Limits::none())?)) + } + + pub fn xdr(&self) -> Option<&str> { + match self { + TxnResult::Xdr(xdr) => Some(xdr), + TxnResult::Res(_) => None, + } + } + + pub fn res(self) -> Option { + match self { + TxnResult::Res(res) => Some(res), + TxnResult::Xdr(_) => None, + } + } + + pub fn try_xdr(&self) -> Result<&str, Error> { + self.xdr().ok_or(Error::XdrStringExpected) + } + + pub fn try_res(self) -> Result { + self.res().ok_or(Error::ResultExpected) + } +} + +impl Display for TxnResult +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TxnResult::Xdr(xdr) => write!(f, "{xdr}"), + TxnResult::Res(res) => write!(f, "{res}"), + } + } +} diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index 353fe6e5db..f6ac76d58f 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -1,5 +1,6 @@ use clap::arg; -use soroban_env_host::xdr; + +use soroban_env_host::xdr::{self, WriteXdr}; use soroban_rpc::Assembled; use crate::commands::HEADING_RPC; @@ -16,15 +17,31 @@ pub struct Args { /// Number of instructions to simulate #[arg(long, help_heading = HEADING_RPC)] pub instructions: Option, + /// Build the transaction only write the base64 xdr to stdout + #[arg(long, help_heading = HEADING_RPC)] + pub build_only: bool, + /// Simulation the transaction only write the base64 xdr to stdout + #[arg(long, help_heading = HEADING_RPC, conflicts_with = "build_only")] + pub sim_only: bool, } impl Args { - pub fn apply_to_assembled_txn(&self, txn: Assembled) -> Assembled { - if let Some(instructions) = self.instructions { + pub fn apply_to_assembled_txn(&self, txn: Assembled) -> Result { + let simulated_txn = if let Some(instructions) = self.instructions { txn.set_max_instructions(instructions) } else { add_padding_to_instructions(txn) + }; + if self.sim_only { + println!( + "{}", + simulated_txn + .transaction() + .to_xdr_base64(xdr::Limits::none())? + ); + std::process::exit(0); } + Ok(simulated_txn) } } @@ -47,6 +64,8 @@ impl Default for Args { fee: 100, cost: false, instructions: None, + build_only: false, + sim_only: false, } } } diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index d4118a6be8..01bdaae1c2 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -5,6 +5,8 @@ )] pub(crate) use soroban_env_host::xdr; pub(crate) use soroban_rpc as rpc; +pub(crate) use soroban_env_host::xdr; +pub(crate) use soroban_rpc as rpc; use std::path::Path; pub mod commands; diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index 766a7798ee..9d10a653e7 100644 --- a/docs/soroban-cli-full-docs.md +++ b/docs/soroban-cli-full-docs.md @@ -240,6 +240,14 @@ Deploy builtin Soroban Asset Contract Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + @@ -391,6 +399,14 @@ If no keys are specified the contract itself is extended. Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + @@ -423,6 +439,14 @@ Deploy a wasm contract Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + * `-i`, `--ignore-checks` — Whether to ignore safety checks when deploying contracts Default value: `false` @@ -587,6 +611,14 @@ Install a WASM file to the ledger without creating a contract instance Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + * `--wasm ` — Path to wasm binary * `-i`, `--ignore-checks` — Whether to ignore safety checks when deploying contracts @@ -636,6 +668,14 @@ soroban contract invoke ... -- --help Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + @@ -748,6 +788,14 @@ If no keys are specificed the contract itself is restored. Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + @@ -1339,6 +1387,9 @@ Read cached action * `--id ` — ID of the cache entry + Possible values: `envelope` + +