From 199a88a0be72c4caaef59c1e918e199e4e247078 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 9 May 2024 08:31:22 -0400 Subject: [PATCH] feat: add no-build option for fee::Args (#1267) * feat: --no-build command allows returning built transaction --sim-only for assembling transactions * fix: clippy and fmt * fix: address PR comments and skip install step for build only deploy --- cmd/crates/soroban-spec-tools/src/lib.rs | 3 +- cmd/crates/soroban-test/src/lib.rs | 4 +- .../tests/it/integration/hello_world.rs | 27 ++++- .../soroban-test/tests/it/integration/util.rs | 7 +- cmd/crates/soroban-test/tests/it/util.rs | 5 +- .../src/commands/contract/deploy/asset.rs | 16 ++- .../src/commands/contract/deploy/wasm.rs | 43 ++++++-- .../src/commands/contract/extend.rs | 37 ++++--- .../src/commands/contract/fetch.rs | 1 - .../src/commands/contract/id/asset.rs | 9 +- .../src/commands/contract/install.rs | 66 ++++++----- .../src/commands/contract/invoke.rs | 104 ++++++++++++------ cmd/soroban-cli/src/commands/contract/read.rs | 3 +- .../src/commands/contract/restore.rs | 31 ++++-- cmd/soroban-cli/src/commands/events.rs | 4 +- cmd/soroban-cli/src/commands/mod.rs | 2 + cmd/soroban-cli/src/commands/txn_result.rs | 35 ++++++ cmd/soroban-cli/src/fee.rs | 9 ++ cmd/soroban-cli/src/lib.rs | 3 +- cmd/soroban-cli/src/wasm.rs | 7 +- docs/soroban-cli-full-docs.md | 48 ++++++++ 21 files changed, 344 insertions(+), 120 deletions(-) create mode 100644 cmd/soroban-cli/src/commands/txn_result.rs diff --git a/cmd/crates/soroban-spec-tools/src/lib.rs b/cmd/crates/soroban-spec-tools/src/lib.rs index e6d496437..c227c3478 100644 --- a/cmd/crates/soroban-spec-tools/src/lib.rs +++ b/cmd/crates/soroban-spec-tools/src/lib.rs @@ -1137,8 +1137,7 @@ impl Spec { ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 { fields, .. }) if fields .first() - .map(|f| f.name.to_utf8_string_lossy() == "0") - .unwrap_or_default() => + .is_some_and(|f| f.name.to_utf8_string_lossy() == "0") => { let fields = fields .iter() diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index e4d7410ce..d55ad7bc7 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -193,7 +193,9 @@ impl TestEnv { source: &str, ) -> Result { let cmd = self.cmd_with_config::(command_str); - self.run_cmd_with(cmd, source).await + self.run_cmd_with(cmd, source) + .await + .map(|r| r.into_result().unwrap()) } /// A convenience method for using the invoke command. 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 be03afa8d..fae0a8463 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -2,6 +2,7 @@ use predicates::boolean::PredicateBooleanExt; use soroban_cli::commands::{ config::{locator, secret}, contract::{self, fetch}, + txn_result::TxnResult, }; use soroban_rpc::GetLatestLedgerResponse; use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; @@ -11,6 +12,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, TxnResult::Res(format!(r#"["Hello",{world:?}]"#))); +} + #[tokio::test] async fn invoke() { let sandbox = &TestEnv::new(); @@ -140,14 +153,18 @@ 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"]"#); + assert_eq!(res, TxnResult::Res(r#"["Hello","world"]"#.to_string())); } fn invoke_auth(sandbox: &TestEnv, id: &str, addr: &str) { diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index faa3534e4..4fc870e34 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -38,7 +38,12 @@ pub async fn deploy_contract(sandbox: &TestEnv, wasm: &Wasm<'static>) -> String TEST_SALT, "--ignore-checks", ]); - sandbox.run_cmd_with(cmd, "test").await.unwrap() + sandbox + .run_cmd_with(cmd, "test") + .await + .unwrap() + .into_result() + .unwrap() } pub async fn extend_contract(sandbox: &TestEnv, id: &str) { diff --git a/cmd/crates/soroban-test/tests/it/util.rs b/cmd/crates/soroban-test/tests/it/util.rs index c70091503..c2f01054c 100644 --- a/cmd/crates/soroban-test/tests/it/util.rs +++ b/cmd/crates/soroban-test/tests/it/util.rs @@ -53,7 +53,10 @@ pub async fn invoke_custom( ) -> Result { let mut i: contract::invoke::Cmd = sandbox.cmd_with_config(&["--id", id, "--", func, arg]); i.wasm = Some(wasm.to_path_buf()); - sandbox.run_cmd_with(i, TEST_ACCOUNT).await + sandbox + .run_cmd_with(i, TEST_ACCOUNT) + .await + .map(|r| r.into_result().unwrap()) } pub const DEFAULT_CONTRACT_ID: &str = "CDR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OO5Z"; diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 41e7367dc..1f8d4544f 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}, @@ -74,13 +76,13 @@ impl Cmd { #[async_trait::async_trait] impl NetworkRunnable for Cmd { type Error = Error; - type Result = String; + type Result = TxnResult; async fn run_against_rpc_server( &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result { let config = config.unwrap_or(&self.config); // Parse asset let asset = parse_asset(&self.asset)?; @@ -108,8 +110,14 @@ impl NetworkRunnable for Cmd { network_passphrase, &key, )?; + if self.fee.build_only { + return Ok(TxnResult::Txn(tx)); + } let txn = client.create_assembled_transaction(&tx).await?; let txn = self.fee.apply_to_assembled_txn(txn); + if self.fee.sim_only { + return Ok(TxnResult::Txn(txn.transaction().clone())); + } let get_txn_resp = client .send_assembled_transaction(txn, &key, &[], network_passphrase, None, None) .await? @@ -118,7 +126,7 @@ impl NetworkRunnable for Cmd { 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))) } } diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 40369b230..dc4632043 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::TxnResult, + NetworkRunnable, }; use crate::{ commands::{config, contract::install, HEADING_RPC}, @@ -96,6 +98,8 @@ pub enum Error { Data(#[from] data::Error), #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] + Wasm(#[from] wasm::Error), } impl Cmd { @@ -109,23 +113,29 @@ impl Cmd { #[async_trait::async_trait] impl NetworkRunnable for Cmd { type Error = Error; - type Result = String; + type Result = TxnResult; async fn run_against_rpc_server( &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 hash = install::Cmd { - wasm: wasm::Args { wasm: wasm.clone() }, - config: config.clone(), - fee: self.fee.clone(), - ignore_checks: self.ignore_checks, - } - .run_against_rpc_server(global_args, Some(config)) - .await?; + let hash = if self.fee.build_only { + wasm::Args { wasm: wasm.clone() }.hash()? + } else { + install::Cmd { + wasm: wasm::Args { wasm: wasm.clone() }, + config: config.clone(), + fee: self.fee.clone(), + ignore_checks: self.ignore_checks, + } + .run_against_rpc_server(global_args, Some(config)) + .await? + .into_result() + .expect("the value (hash) is expected because it should always be available since build-only is a shared parameter") + }; hex::encode(hash) } else { self.wasm_hash @@ -169,8 +179,15 @@ impl NetworkRunnable for Cmd { salt, &key, )?; + if self.fee.build_only { + return Ok(TxnResult::Txn(txn)); + } + let txn = client.create_assembled_transaction(&txn).await?; let txn = self.fee.apply_to_assembled_txn(txn); + if self.fee.sim_only { + return Ok(TxnResult::Txn(txn.transaction().clone())); + } let get_txn_resp = client .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) .await? @@ -178,7 +195,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 ab7834959..117568c20 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -3,15 +3,17 @@ use std::{fmt::Debug, path::Path, str::FromStr}; use clap::{command, Parser}; use soroban_env_host::xdr::{ Error as XdrError, ExtendFootprintTtlOp, ExtensionPoint, LedgerEntry, LedgerEntryChange, - LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, Preconditions, - SequenceNumber, SorobanResources, SorobanTransactionData, Transaction, TransactionExt, - TransactionMeta, TransactionMetaV3, TtlEntry, Uint256, + LedgerEntryData, LedgerFootprint, Limits, Memo, MuxedAccount, Operation, OperationBody, + Preconditions, SequenceNumber, SorobanResources, SorobanTransactionData, Transaction, + TransactionExt, TransactionMeta, TransactionMetaV3, TtlEntry, Uint256, WriteXdr, }; use crate::{ commands::{ config::{self, data}, - global, network, NetworkRunnable, + global, network, + txn_result::TxnResult, + NetworkRunnable, }, key, rpc::{self, Client}, @@ -87,11 +89,16 @@ 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?; - if self.ttl_ledger_only { - println!("{ttl_ledger}"); - } else { - println!("New ttl ledger: {ttl_ledger}"); + let res = self.run_against_rpc_server(None, None).await?; + match res { + TxnResult::Txn(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?), + TxnResult::Res(ttl_ledger) => { + if self.ttl_ledger_only { + println!("{ttl_ledger}"); + } else { + println!("New ttl ledger: {ttl_ledger}"); + } + } } Ok(()) @@ -111,13 +118,13 @@ impl Cmd { #[async_trait::async_trait] impl NetworkRunnable for Cmd { type Error = Error; - type Result = u32; + type Result = TxnResult; async fn run_against_rpc_server( &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 +168,9 @@ impl NetworkRunnable for Cmd { resource_fee: 0, }), }; - + if self.fee.build_only { + return Ok(TxnResult::Txn(tx)); + } let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; @@ -194,7 +203,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 +218,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 eefb1b4b8..73ba12c2d 100644 --- a/cmd/soroban-cli/src/commands/contract/fetch.rs +++ b/cmd/soroban-cli/src/commands/contract/fetch.rs @@ -145,7 +145,6 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - // async closures are not yet stable Ok(client.get_remote_wasm(&contract_id).await?) } } diff --git a/cmd/soroban-cli/src/commands/contract/id/asset.rs b/cmd/soroban-cli/src/commands/contract/id/asset.rs index 34e5767a6..e036b7939 100644 --- a/cmd/soroban-cli/src/commands/contract/id/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/id/asset.rs @@ -26,11 +26,14 @@ pub enum Error { } impl Cmd { pub fn run(&self) -> Result<(), Error> { + println!("{}", self.contract_address()?); + Ok(()) + } + + pub fn contract_address(&self) -> Result { let asset = parse_asset(&self.asset)?; let network = self.config.get_network()?; let contract_id = contract_id_hash_from_asset(&asset, &network.network_passphrase)?; - let strkey_contract_id = stellar_strkey::Contract(contract_id.0).to_string(); - println!("{strkey_contract_id}"); - Ok(()) + Ok(stellar_strkey::Contract(contract_id.0)) } } diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 8763fd7e6..4e1b83d9a 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -7,11 +7,12 @@ use soroban_env_host::xdr::{ self, ContractCodeEntryExt, Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, LedgerEntryData, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions, ReadXdr, ScMetaEntry, ScMetaV0, SequenceNumber, Transaction, TransactionExt, TransactionResult, - TransactionResultResult, Uint256, VecM, + TransactionResultResult, Uint256, VecM, WriteXdr, }; 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::Txn(tx) => tx.to_xdr_base64(Limits::none())?, + TxnResult::Res(hash) => hex::encode(hash), + }; println!("{res_str}"); Ok(()) } @@ -81,12 +85,12 @@ impl Cmd { #[async_trait::async_trait] impl NetworkRunnable for Cmd { type Error = Error; - type Result = Hash; + type Result = TxnResult; async fn run_against_rpc_server( &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,37 +129,47 @@ impl NetworkRunnable for Cmd { let (tx_without_preflight, hash) = build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &key)?; - let code_key = - xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() }); - let contract_data = client.get_ledger_entries(&[code_key]).await?; - // Skip install if the contract is already installed, and the contract has an extension version that isn't V0. - // In protocol 21 extension V1 was added that stores additional information about a contract making execution - // of the contract cheaper. So if folks want to reinstall we should let them which is why the install will still - // go ahead if the contract has a V0 extension. - if let Some(entries) = contract_data.entries { - if let Some(entry_result) = entries.first() { - let entry: LedgerEntryData = - LedgerEntryData::from_xdr_base64(&entry_result.xdr, Limits::none())?; + if self.fee.build_only { + return Ok(TxnResult::Txn(tx_without_preflight)); + } + // Don't check whether the contract is already installed when the user + // has requested to perform simulation only and is hoping to get a + // transaction back. + if !self.fee.sim_only { + let code_key = + xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() }); + let contract_data = client.get_ledger_entries(&[code_key]).await?; + // Skip install if the contract is already installed, and the contract has an extension version that isn't V0. + // In protocol 21 extension V1 was added that stores additional information about a contract making execution + // of the contract cheaper. So if folks want to reinstall we should let them which is why the install will still + // go ahead if the contract has a V0 extension. + if let Some(entries) = contract_data.entries { + if let Some(entry_result) = entries.first() { + let entry: LedgerEntryData = + LedgerEntryData::from_xdr_base64(&entry_result.xdr, Limits::none())?; - match &entry { - LedgerEntryData::ContractCode(code) => { - // Skip reupload if this isn't V0 because V1 extension already - // exists. - if code.ext.ne(&ContractCodeEntryExt::V0) { - return Ok(hash); + match &entry { + LedgerEntryData::ContractCode(code) => { + // Skip reupload if this isn't V0 because V1 extension already + // exists. + if code.ext.ne(&ContractCodeEntryExt::V0) { + return Ok(TxnResult::Res(hash)); + } + } + _ => { + tracing::warn!("Entry retrieved should be of type ContractCode"); } - } - _ => { - tracing::warn!("Entry retrieved should be of type ContractCode"); } } } } - let txn = client .create_assembled_transaction(&tx_without_preflight) .await?; let txn = self.fee.apply_to_assembled_txn(txn); + if self.fee.sim_only { + return Ok(TxnResult::Txn(txn.transaction().clone())); + } let txn_resp = client .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) .await?; @@ -189,7 +203,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 d40f9c46f..ef1fd7db1 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -12,15 +12,18 @@ 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_env_host::xdr::{ + AccountEntry, AccountEntryExt, AccountId, ContractDataEntry, DiagnosticEvent, Thresholds, +}; use soroban_spec::read::FromWasmError; use stellar_strkey::DecodeError; @@ -28,6 +31,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 +84,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 +113,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 +173,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 +206,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 +277,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 } @@ -303,13 +308,13 @@ impl Cmd { #[async_trait::async_trait] impl NetworkRunnable for Cmd { type Error = Error; - type Result = String; + type Result = TxnResult; async fn run_against_rpc_server( &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 +322,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 +371,21 @@ 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::Txn(tx)); + } let txn = client.create_assembled_transaction(&tx).await?; let txn = self.fee.apply_to_assembled_txn(txn); + if self.fee.sim_only { + return Ok(TxnResult::Txn(txn.transaction().clone())); + } 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 +402,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 +420,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 +451,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 +466,14 @@ pub fn output_to_string(spec: &Spec, res: &ScVal, function: &str) -> Result Result { let op = Operation { source_account: None, @@ -446,7 +483,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 +543,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 a7b1d07a8..d7daacd3f 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -7,11 +7,10 @@ use clap::{command, Parser, ValueEnum}; use soroban_env_host::{ xdr::{ ContractDataEntry, Error as XdrError, LedgerEntryData, LedgerKey, LedgerKeyContractData, - ScVal, WriteXdr, + Limits, ScVal, WriteXdr, }, HostError, }; -use soroban_sdk::xdr::Limits; use crate::{ commands::{config, global, NetworkRunnable}, diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 8b5921a1a..f78316886 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -3,9 +3,9 @@ use std::{fmt::Debug, path::Path, str::FromStr}; use clap::{command, Parser}; use soroban_env_host::xdr::{ Error as XdrError, ExtensionPoint, LedgerEntry, LedgerEntryChange, LedgerEntryData, - LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, OperationMeta, Preconditions, - RestoreFootprintOp, SequenceNumber, SorobanResources, SorobanTransactionData, Transaction, - TransactionExt, TransactionMeta, TransactionMetaV3, TtlEntry, Uint256, + LedgerFootprint, Limits, Memo, MuxedAccount, Operation, OperationBody, OperationMeta, + Preconditions, RestoreFootprintOp, SequenceNumber, SorobanResources, SorobanTransactionData, + Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TtlEntry, Uint256, WriteXdr, }; use stellar_strkey::DecodeError; @@ -13,7 +13,9 @@ use crate::{ commands::{ config::{self, data, locator}, contract::extend, - global, network, NetworkRunnable, + global, network, + txn_result::TxnResult, + NetworkRunnable, }, key, rpc::{self, Client}, @@ -92,8 +94,13 @@ pub enum 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::Txn(xdr) => { + println!("{}", xdr.to_xdr_base64(Limits::none())?); + return Ok(()); + } + }; if let Some(ledgers_to_extend) = self.ledgers_to_extend { extend::Cmd { key: self.key.clone(), @@ -115,13 +122,13 @@ impl Cmd { #[async_trait::async_trait] impl NetworkRunnable for Cmd { type Error = Error; - type Result = u32; + type Result = TxnResult; async fn run_against_rpc_server( &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 +169,9 @@ impl NetworkRunnable for Cmd { resource_fee: 0, }), }; - + if self.fee.build_only { + return Ok(TxnResult::Txn(tx)); + } let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; @@ -198,7 +207,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 42145f5bf..23cd07e9d 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -226,7 +226,7 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - client + Ok(client .get_events( start, Some(self.event_type), @@ -235,6 +235,6 @@ impl NetworkRunnable for Cmd { Some(self.count), ) .await - .map_err(Error::Rpc) + .map_err(Error::Rpc)?) } } diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 4bb5dcb53..37328cd1a 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. 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 000000000..6b189f25b --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn_result.rs @@ -0,0 +1,35 @@ +use std::fmt::{Display, Formatter}; + +use soroban_env_host::xdr::{Limits, Transaction, WriteXdr}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum TxnResult { + Txn(Transaction), + Res(R), +} + +impl TxnResult { + pub fn into_result(self) -> Option { + match self { + TxnResult::Res(res) => Some(res), + TxnResult::Txn(_) => None, + } + } +} + +impl Display for TxnResult +where + V: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TxnResult::Txn(tx) => write!( + f, + "{}", + tx.to_xdr_base64(Limits::none()) + .map_err(|_| std::fmt::Error)? + ), + TxnResult::Res(value) => write!(f, "{value}"), + } + } +} diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index 353fe6e5d..b7b7e0441 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -1,4 +1,5 @@ use clap::arg; + use soroban_env_host::xdr; use soroban_rpc::Assembled; @@ -16,6 +17,12 @@ 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 { @@ -47,6 +54,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 d4118a6be..5cde45436 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -3,9 +3,10 @@ clippy::must_use_candidate, clippy::missing_panics_doc )] +use std::path::Path; + pub(crate) use soroban_env_host::xdr; pub(crate) use soroban_rpc as rpc; -use std::path::Path; pub mod commands; pub mod fee; diff --git a/cmd/soroban-cli/src/wasm.rs b/cmd/soroban-cli/src/wasm.rs index 4b8a7f8ca..6f6daf462 100644 --- a/cmd/soroban-cli/src/wasm.rs +++ b/cmd/soroban-cli/src/wasm.rs @@ -1,5 +1,6 @@ use clap::arg; -use soroban_env_host::xdr::{self, LedgerKey, LedgerKeyContractCode}; +use sha2::{Digest, Sha256}; +use soroban_env_host::xdr::{self, Hash, LedgerKey, LedgerKeyContractCode}; use soroban_spec_tools::contract::{self, Spec}; use std::{ fs, io, @@ -65,6 +66,10 @@ impl Args { let contents = self.read()?; Ok(Spec::new(&contents)?) } + + pub fn hash(&self) -> Result { + Ok(Hash(Sha256::digest(self.read()?).into())) + } } impl From<&PathBuf> for Args { diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index 766a7798e..b005f1e7f 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` +