From 7948f0aeac660346cbf62f9037464580755fddc6 Mon Sep 17 00:00:00 2001 From: Bot Date: Fri, 23 Oct 2026 18:57:58 +0800 Subject: [PATCH] feat(contract): add --simulate-only option to deploy and upload commands --- .../src/commands/contract/deploy/asset.rs | 63 ++++++++++++---- .../src/commands/contract/deploy/wasm.rs | 75 +++++++++++++++---- .../src/commands/contract/upload.rs | 44 +++++++++++ 3 files changed, 152 insertions(+), 30 deletions(-) diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 8b52e8bfe0..fd5eb0c57a 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -94,6 +94,15 @@ pub struct Cmd { /// Build the transaction and only write the base64 xdr to stdout #[arg(long, help_heading = HEADING_TRANSACTION)] pub build_only: bool, + + /// Simulate the transaction only without signing or submitting to the network + #[arg( + long, + visible_alias = "is-view", + visible_alias = "sim-only", + help_heading = HEADING_TRANSACTION + )] + pub simulate_only: bool, } impl Cmd { @@ -115,23 +124,25 @@ impl Cmd { TxnEnvelopeResult::Res(contract) => { let network = self.config.get_network()?; - if let Some(alias) = self.alias.clone() { - if let Some(existing_contract) = self - .config - .locator - .get_contract_id(&alias, &network.network_passphrase)? - { - let print = Print::new(global_args.quiet); - print.warnln(format!( - "Overwriting existing contract id: {existing_contract}" - )); + if !self.simulate_only { + if let Some(alias) = self.alias.clone() { + if let Some(existing_contract) = self + .config + .locator + .get_contract_id(&alias, &network.network_passphrase)? + { + let print = Print::new(global_args.quiet); + print.warnln(format!( + "Overwriting existing contract id: {existing_contract}" + )); + } + + self.config.locator.save_contract_id( + &network.network_passphrase, + &contract, + &alias, + )?; } - - self.config.locator.save_contract_id( - &network.network_passphrase, - &contract, - &alias, - )?; } println!("{contract}"); @@ -186,6 +197,26 @@ impl Cmd { return Ok(TxnResult::Txn(Box::new(tx))); } + if self.simulate_only { + print.infoln("Simulating transaction…"); + let assembled = crate::assembled::simulate_and_assemble_transaction( + &client, + &tx, + self.resources.resource_config(), + self.resources.resource_fee, + None, + ) + .await?; + if let Ok(events) = assembled.sim_res.events() { + crate::log::event::all(&events); + } + let fee = assembled.transaction().fee; + print.checkln(format!( + "Simulation successful! Estimated fee: {fee} stroops" + )); + return Ok(TxnResult::Res(stellar_strkey::Contract(contract_id.0))); + } + sim_sign_and_send_tx::( &client, &tx, diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 4655a2c123..17367505a4 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -72,6 +72,15 @@ pub struct Cmd { /// Build the transaction and only write the base64 xdr to stdout #[arg(long, help_heading = HEADING_TRANSACTION)] pub build_only: bool, + + /// Simulate the transaction only without signing or submitting to the network + #[arg( + long, + visible_alias = "is-view", + visible_alias = "sim-only", + help_heading = HEADING_TRANSACTION + )] + pub simulate_only: bool, /// If provided, will be passed to the contract's `__constructor` function with provided arguments for that function as `--arg-name value` #[arg(last = true, id = "CONTRACT_CONSTRUCTOR_ARGS")] pub slop: Vec, @@ -262,23 +271,25 @@ impl Cmd { TxnEnvelopeResult::Res(contract) => { let network = cmd.config.get_network()?; - if let Some(alias) = cmd.alias.clone() { - if let Some(existing_contract) = cmd - .config - .locator - .get_contract_id(&alias, &network.network_passphrase)? - { - let print = Print::new(global_args.quiet); - print.warnln(format!( + if !cmd.simulate_only { + if let Some(alias) = cmd.alias.clone() { + if let Some(existing_contract) = cmd + .config + .locator + .get_contract_id(&alias, &network.network_passphrase)? + { + let print = Print::new(global_args.quiet); + print.warnln(format!( "Overwriting existing alias '{alias}' that currently links to contract ID: {existing_contract}" )); - } + } - cmd.config.locator.save_contract_id( - &network.network_passphrase, - &contract, - &alias, - )?; + cmd.config.locator.save_contract_id( + &network.network_passphrase, + &contract, + &alias, + )?; + } } println!("{contract}"); @@ -346,6 +357,7 @@ impl Cmd { auth_mode: self.auth_mode.clone(), ignore_checks: self.ignore_checks, build_only: is_build, + simulate_only: self.simulate_only, package: None, build_args: build::BuildArgs::default(), } @@ -445,6 +457,26 @@ impl Cmd { return Ok(TxnResult::Txn(txn)); } + if self.simulate_only { + print.infoln("Simulating transaction…"); + let assembled = crate::assembled::simulate_and_assemble_transaction( + &client, + &txn, + self.resources.resource_config(), + self.resources.resource_fee, + self.auth_mode.to_rpc(), + ) + .await?; + if let Ok(events) = assembled.sim_res.events() { + crate::log::event::all(&events); + } + let fee = assembled.transaction().fee; + print.checkln(format!( + "Simulation successful! Estimated fee: {fee} stroops" + )); + return Ok(TxnResult::Res(contract_id)); + } + sim_sign_and_send_tx::( &client, &txn, @@ -597,4 +629,19 @@ mod tests { assert_eq!(reserved_package_alias(Some(&alias), &contracts), None); } + + #[test] + fn parse_simulate_only_and_aliases() { + let cmd = + Cmd::try_parse_from(["deploy", "--source-account", "alice", "--wasm", "test.wasm", "--simulate-only"]).unwrap(); + assert!(cmd.simulate_only); + + let cmd_is_view = + Cmd::try_parse_from(["deploy", "--source-account", "alice", "--wasm", "test.wasm", "--is-view"]).unwrap(); + assert!(cmd_is_view.simulate_only); + + let cmd_sim_only = + Cmd::try_parse_from(["deploy", "--source-account", "alice", "--wasm", "test.wasm", "--sim-only"]).unwrap(); + assert!(cmd_sim_only.simulate_only); + } } diff --git a/cmd/soroban-cli/src/commands/contract/upload.rs b/cmd/soroban-cli/src/commands/contract/upload.rs index a61120b8db..d145800720 100644 --- a/cmd/soroban-cli/src/commands/contract/upload.rs +++ b/cmd/soroban-cli/src/commands/contract/upload.rs @@ -58,6 +58,15 @@ pub struct Cmd { #[arg(long, help_heading = HEADING_TRANSACTION)] pub build_only: bool, + /// Simulate the transaction only without signing or submitting to the network + #[arg( + long, + visible_alias = "is-view", + visible_alias = "sim-only", + help_heading = HEADING_TRANSACTION + )] + pub simulate_only: bool, + /// Package to build when --wasm is not provided #[arg(long, help_heading = "Build Options", conflicts_with = "wasm")] pub package: Option, @@ -267,6 +276,26 @@ impl Cmd { return Ok(TxnResult::Txn(Box::new(tx_without_preflight))); } + if self.simulate_only { + print.infoln("Simulating transaction…"); + let assembled = crate::assembled::simulate_and_assemble_transaction( + &client, + &tx_without_preflight, + self.resources.resource_config(), + self.resources.resource_fee, + self.auth_mode.to_rpc(), + ) + .await?; + if let Ok(events) = assembled.sim_res.events() { + crate::log::event::all(&events); + } + let fee = assembled.transaction().fee; + print.checkln(format!( + "Simulation successful! Estimated fee: {fee} stroops" + )); + return Ok(TxnResult::Res(hash)); + } + let should_check = true; if should_check { @@ -441,4 +470,19 @@ mod tests { // "rc" detection (used to gate the release-candidate check) still works. assert!(version.contains("rc")); } + + #[test] + fn parse_simulate_only_and_aliases() { + let cmd = + Cmd::try_parse_from(["upload", "--source-account", "alice", "--wasm", "test.wasm", "--simulate-only"]).unwrap(); + assert!(cmd.simulate_only); + + let cmd_is_view = + Cmd::try_parse_from(["upload", "--source-account", "alice", "--wasm", "test.wasm", "--is-view"]).unwrap(); + assert!(cmd_is_view.simulate_only); + + let cmd_sim_only = + Cmd::try_parse_from(["upload", "--source-account", "alice", "--wasm", "test.wasm", "--sim-only"]).unwrap(); + assert!(cmd_sim_only.simulate_only); + } }