diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index d12880f789..39b958cacc 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -10,6 +10,7 @@ use clap::{arg, command, Parser}; use std::convert::Infallible; use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; +use crate::commands::tx::fetch; use crate::{ assembled::simulate_and_assemble_transaction, commands::{ @@ -29,24 +30,39 @@ use crate::commands::contract::deploy::utils::alias_validator; pub enum Error { #[error("error parsing int: {0}")] ParseIntError(#[from] ParseIntError), + #[error(transparent)] Client(#[from] SorobanRpcError), + #[error("internal conversion error: {0}")] TryFromSliceError(#[from] TryFromSliceError), + #[error("xdr processing error: {0}")] Xdr(#[from] XdrError), + #[error(transparent)] Config(#[from] config::Error), + #[error(transparent)] Data(#[from] data::Error), + #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] Builder(#[from] builder::Error), + #[error(transparent)] Asset(#[from] builder::asset::Error), + #[error(transparent)] Locator(#[from] locator::Error), + + #[error(transparent)] + Fee(#[from] fetch::fee::Error), + + #[error(transparent)] + Fetch(#[from] fetch::Error), } impl From for Error { @@ -152,16 +168,19 @@ impl NetworkRunnable for Cmd { return Ok(TxnResult::Txn(Box::new(tx))); } - let txn = + let assembled = simulate_and_assemble_transaction(&client, &tx, self.fee.resource_config()).await?; - let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); + let assembled = self.fee.apply_to_assembled_txn(assembled); + + let txn = assembled.transaction().clone(); let get_txn_resp = client .send_transaction_polling(&self.config.sign(txn).await?) - .await? - .try_into()?; + .await?; + + self.fee.print_cost_info(&get_txn_resp)?; if args.is_none_or(|a| !a.no_cache) { - data::write(get_txn_resp, &network.rpc_uri()?)?; + data::write(get_txn_resp.clone().try_into()?, &network.rpc_uri()?)?; } 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 afe7edc6d9..32f07e0253 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -14,8 +14,7 @@ use crate::xdr::{ use clap::{arg, command, Parser}; use rand::Rng; -use soroban_spec_tools::contract as contract_spec; - +use crate::commands::tx::fetch; use crate::{ assembled::simulate_and_assemble_transaction, commands::{ @@ -30,6 +29,7 @@ use crate::{ utils::{self, rpc::get_remote_wasm_from_hash}, wasm, }; +use soroban_spec_tools::contract as contract_spec; pub const CONSTRUCTOR_FUNCTION_NAME: &str = "__constructor"; @@ -74,52 +74,78 @@ pub struct Cmd { pub enum Error { #[error(transparent)] Install(#[from] upload::Error), + #[error("error parsing int: {0}")] ParseIntError(#[from] ParseIntError), + #[error("internal conversion error: {0}")] TryFromSliceError(#[from] TryFromSliceError), + #[error("xdr processing error: {0}")] Xdr(#[from] XdrError), + #[error("jsonrpc error: {0}")] JsonRpc(#[from] jsonrpsee_core::Error), + #[error("cannot parse salt: {salt}")] CannotParseSalt { salt: String }, + #[error("cannot parse contract ID {contract_id}: {error}")] CannotParseContractId { contract_id: String, error: stellar_strkey::DecodeError, }, + #[error("cannot parse WASM hash {wasm_hash}: {error}")] CannotParseWasmHash { wasm_hash: String, error: stellar_strkey::DecodeError, }, + #[error("Must provide either --wasm or --wash-hash")] WasmNotProvided, + #[error(transparent)] Rpc(#[from] rpc::Error), + #[error(transparent)] Config(#[from] config::Error), + #[error(transparent)] StrKey(#[from] stellar_strkey::DecodeError), + #[error(transparent)] Infallible(#[from] std::convert::Infallible), + #[error(transparent)] WasmId(#[from] contract::id::wasm::Error), + #[error(transparent)] Data(#[from] data::Error), + #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] Wasm(#[from] wasm::Error), + #[error(transparent)] Locator(#[from] locator::Error), + #[error(transparent)] ContractSpec(#[from] contract_spec::Error), + #[error(transparent)] ArgParse(#[from] arg_parsing::Error), + #[error("Only ed25519 accounts are allowed")] OnlyEd25519AccountsAllowed, + + #[error(transparent)] + Fee(#[from] fetch::fee::Error), + + #[error(transparent)] + Fetch(#[from] fetch::Error), } impl Cmd { @@ -282,21 +308,22 @@ impl NetworkRunnable for Cmd { print.infoln("Simulating deploy transaction…"); - let txn = + let assembled = simulate_and_assemble_transaction(&client, &txn, self.fee.resource_config()).await?; - let txn = Box::new(self.fee.apply_to_assembled_txn(txn).transaction().clone()); + let assembled = self.fee.apply_to_assembled_txn(assembled); + + let txn = Box::new(assembled.transaction().clone()); print.log_transaction(&txn, &network, true)?; let signed_txn = &config.sign(*txn).await?; print.globeln("Submitting deploy transaction…"); - let get_txn_resp = client - .send_transaction_polling(signed_txn) - .await? - .try_into()?; + let get_txn_resp = client.send_transaction_polling(signed_txn).await?; + + self.fee.print_cost_info(&get_txn_resp)?; if global_args.is_none_or(|a| !a.no_cache) { - data::write(get_txn_resp, &network.rpc_uri()?)?; + data::write(get_txn_resp.clone().try_into()?, &network.rpc_uri()?)?; } if let Some(url) = utils::explorer_url_for_contract(&network, &contract_id) { diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 7265f5cc07..a887819554 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -14,6 +14,7 @@ use crate::{ }; use clap::{command, Parser}; +use crate::commands::tx::fetch; use crate::{ assembled::simulate_and_assemble_transaction, commands::{ @@ -31,13 +32,17 @@ pub struct Cmd { /// Number of ledgers to extend the entries #[arg(long, required = true)] pub ledgers_to_extend: u32, + /// Only print the new Time To Live ledger #[arg(long)] pub ttl_ledger_only: bool, + #[command(flatten)] pub key: key::Args, + #[command(flatten)] pub config: config::Args, + #[command(flatten)] pub fee: crate::fee::Args, } @@ -64,37 +69,57 @@ pub enum Error { key: String, error: soroban_spec_tools::Error, }, + #[error("parsing XDR key {key}: {error}")] CannotParseXdrKey { key: String, error: XdrError }, #[error(transparent)] Config(#[from] config::Error), + #[error("either `--key` or `--key-xdr` are required")] KeyIsRequired, + #[error("xdr processing error: {0}")] Xdr(#[from] XdrError), + #[error("Ledger entry not found")] LedgerEntryNotFound, + #[error("missing operation result")] MissingOperationResult, + #[error(transparent)] Rpc(#[from] rpc::Error), + #[error(transparent)] Wasm(#[from] wasm::Error), + #[error(transparent)] Key(#[from] key::Error), + #[error(transparent)] Data(#[from] data::Error), + #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] Locator(#[from] locator::Error), + #[error(transparent)] IntError(#[from] TryFromIntError), + #[error("Failed to fetch state archival settings from network")] StateArchivalSettingsNotFound, + #[error("Ledgers to extend ({requested}) exceeds network maximum ({max})")] LedgersToExtendTooLarge { requested: u32, max: u32 }, + + #[error(transparent)] + Fee(#[from] fetch::fee::Error), + + #[error(transparent)] + Fetch(#[from] fetch::Error), } impl Cmd { @@ -210,13 +235,15 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx)); } - let tx = simulate_and_assemble_transaction(&client, &tx, self.fee.resource_config()) - .await? - .transaction() - .clone(); + let assembled = + simulate_and_assemble_transaction(&client, &tx, self.fee.resource_config()).await?; + + let tx = assembled.transaction().clone(); let res = client .send_transaction_polling(&config.sign(tx).await?) .await?; + self.fee.print_cost_info(&res)?; + if args.is_none_or(|a| !a.no_cache) { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; } diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 66682425c1..d45f59517b 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::tx::fetch; use crate::log::extract_events; use crate::print::Print; use crate::utils::deprecate_message; @@ -20,6 +21,7 @@ use crate::{ commands::{ contract::arg_parsing::{build_host_function_parameters, output_to_string}, global, + tx::fetch::fee, txn_result::{TxnEnvelopeResult, TxnResult}, NetworkRunnable, }, @@ -87,49 +89,75 @@ impl Pwd for Cmd { pub enum Error { #[error("cannot add contract to ledger entries: {0}")] CannotAddContractToLedgerEntries(xdr::Error), + #[error("reading file {0:?}: {1}")] CannotReadContractFile(PathBuf, io::Error), + #[error("committing file {filepath}: {error}")] CannotCommitEventsFile { filepath: std::path::PathBuf, error: events::Error, }, + #[error("parsing contract spec: {0}")] CannotParseContractSpec(FromWasmError), + #[error(transparent)] Xdr(#[from] xdr::Error), + #[error("error parsing int: {0}")] ParseIntError(#[from] ParseIntError), + #[error(transparent)] Rpc(#[from] rpc::Error), + #[error("missing operation result")] MissingOperationResult, + #[error("error loading signing key: {0}")] SignatureError(#[from] ed25519_dalek::SignatureError), + #[error(transparent)] Config(#[from] config::Error), + #[error("unexpected ({length}) simulate transaction result length")] UnexpectedSimulateTransactionResultSize { length: usize }, + #[error(transparent)] Clap(#[from] clap::Error), + #[error(transparent)] Locator(#[from] locator::Error), + #[error("Contract Error\n{0}: {1}")] ContractInvoke(String, String), + #[error(transparent)] StrKey(#[from] stellar_strkey::DecodeError), + #[error(transparent)] ContractSpec(#[from] contract::Error), + #[error(transparent)] Io(#[from] std::io::Error), + #[error(transparent)] Data(#[from] data::Error), + #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] GetSpecError(#[from] get_spec::Error), + #[error(transparent)] ArgParsing(#[from] arg_parsing::Error), + + #[error(transparent)] + Fee(#[from] fee::Error), + + #[error(transparent)] + Fetch(#[from] fetch::Error), } impl From for Error { @@ -333,6 +361,8 @@ impl NetworkRunnable for Cmd { .send_transaction_polling(&config.sign(*txn).await?) .await?; + self.fee.print_cost_info(&res)?; + if !no_cache { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; } diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 4a259ac888..935bea493a 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -13,6 +13,7 @@ use crate::{ use clap::{command, Parser}; use stellar_strkey::DecodeError; +use crate::commands::tx::fetch; use crate::{ assembled::simulate_and_assemble_transaction, commands::{ @@ -30,14 +31,18 @@ use crate::{ pub struct Cmd { #[command(flatten)] pub key: key::Args, + /// Number of ledgers to extend the entry #[arg(long)] pub ledgers_to_extend: Option, + /// Only print the new Time To Live ledger #[arg(long)] pub ttl_ledger_only: bool, + #[command(flatten)] pub config: config::Args, + #[command(flatten)] pub fee: crate::fee::Args, } @@ -64,34 +69,54 @@ pub enum Error { key: String, error: soroban_spec_tools::Error, }, + #[error("parsing XDR key {key}: {error}")] CannotParseXdrKey { key: String, error: XdrError }, + #[error("cannot parse contract ID {0}: {1}")] CannotParseContractId(String, DecodeError), + #[error(transparent)] Config(#[from] config::Error), + #[error("either `--key` or `--key-xdr` are required")] KeyIsRequired, + #[error("xdr processing error: {0}")] Xdr(#[from] XdrError), + #[error("Ledger entry not found")] LedgerEntryNotFound, + #[error(transparent)] Locator(#[from] locator::Error), + #[error("missing operation result")] MissingOperationResult, + #[error(transparent)] Rpc(#[from] rpc::Error), + #[error(transparent)] Wasm(#[from] wasm::Error), + #[error(transparent)] Key(#[from] key::Error), + #[error(transparent)] Extend(#[from] extend::Error), + #[error(transparent)] Data(#[from] data::Error), + #[error(transparent)] Network(#[from] network::Error), + + #[error(transparent)] + Fee(#[from] fetch::fee::Error), + + #[error(transparent)] + Fetch(#[from] fetch::Error), } impl Cmd { @@ -177,13 +202,14 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx)); } - let tx = simulate_and_assemble_transaction(&client, &tx, self.fee.resource_config()) - .await? - .transaction() - .clone(); + let assembled = + simulate_and_assemble_transaction(&client, &tx, self.fee.resource_config()).await?; + + let tx = assembled.transaction().clone(); let res = client .send_transaction_polling(&config.sign(tx).await?) .await?; + self.fee.print_cost_info(&res)?; if args.is_none_or(|a| !a.no_cache) { data::write(res.clone().try_into()?, &network.rpc_uri()?)?; } diff --git a/cmd/soroban-cli/src/commands/contract/upload.rs b/cmd/soroban-cli/src/commands/contract/upload.rs index af58650cec..392351e192 100644 --- a/cmd/soroban-cli/src/commands/contract/upload.rs +++ b/cmd/soroban-cli/src/commands/contract/upload.rs @@ -10,6 +10,7 @@ use crate::xdr::{ use clap::{command, Parser}; use super::restore; +use crate::commands::tx::fetch; use crate::{ assembled::simulate_and_assemble_transaction, commands::{ @@ -33,10 +34,13 @@ const PUBLIC_NETWORK_PASSPHRASE: &str = "Public Global Stellar Network ; Septemb pub struct Cmd { #[command(flatten)] pub config: config::Args, + #[command(flatten)] pub fee: crate::fee::Args, + #[command(flatten)] pub wasm: wasm::Args, + #[arg(long, short = 'i', default_value = "false")] /// Whether to ignore safety checks when deploying contracts pub ignore_checks: bool, @@ -46,38 +50,57 @@ pub struct Cmd { pub enum Error { #[error("error parsing int: {0}")] ParseIntError(#[from] ParseIntError), + #[error("internal conversion error: {0}")] TryFromSliceError(#[from] TryFromSliceError), + #[error("xdr processing error: {0}")] Xdr(#[from] XdrError), + #[error("jsonrpc error: {0}")] JsonRpc(#[from] jsonrpsee_core::Error), + #[error(transparent)] Rpc(#[from] rpc::Error), + #[error(transparent)] Config(#[from] config::Error), + #[error(transparent)] Wasm(#[from] wasm::Error), + #[error("unexpected ({length}) simulate transaction result length")] UnexpectedSimulateTransactionResultSize { length: usize }, + #[error(transparent)] Restore(#[from] restore::Error), + #[error("cannot parse WASM file {wasm}: {error}")] CannotParseWasm { wasm: std::path::PathBuf, error: wasm::Error, }, + #[error("the deployed smart contract {wasm} was built with Soroban Rust SDK v{version}, a release candidate version not intended for use with the Stellar Public Network. To deploy anyway, use --ignore-checks")] ContractCompiledWithReleaseCandidateSdk { wasm: std::path::PathBuf, version: String, }, + #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] Data(#[from] data::Error), + #[error(transparent)] Builder(#[from] builder::Error), + + #[error(transparent)] + Fee(#[from] fetch::fee::Error), + + #[error(transparent)] + Fetch(#[from] fetch::Error), } impl Cmd { @@ -186,17 +209,20 @@ impl NetworkRunnable for Cmd { print.infoln("Simulating install transaction…"); - let txn = simulate_and_assemble_transaction( + let assembled = simulate_and_assemble_transaction( &client, &tx_without_preflight, self.fee.resource_config(), ) .await?; - let txn = Box::new(self.fee.apply_to_assembled_txn(txn).transaction().clone()); + let assembled = self.fee.apply_to_assembled_txn(assembled); + + let txn = Box::new(assembled.transaction().clone()); let signed_txn = &self.config.sign(*txn).await?; print.globeln("Submitting install transaction…"); let txn_resp = client.send_transaction_polling(signed_txn).await?; + self.fee.print_cost_info(&txn_resp)?; if args.is_none_or(|a| !a.no_cache) { data::write(txn_resp.clone().try_into().unwrap(), &network.rpc_uri()?)?; diff --git a/cmd/soroban-cli/src/commands/tx/fetch/fee.rs b/cmd/soroban-cli/src/commands/tx/fetch/fee.rs index 9b7d556114..4c365e5f4b 100644 --- a/cmd/soroban-cli/src/commands/tx/fetch/fee.rs +++ b/cmd/soroban-cli/src/commands/tx/fetch/fee.rs @@ -122,7 +122,7 @@ pub struct FeeTable { } impl FeeTable { - fn new_from_transaction_response(resp: &GetTransactionResponse) -> Result { + pub fn new_from_transaction_response(resp: &GetTransactionResponse) -> Result { let (tx_result, tx_meta, tx_envelope) = Self::unpack_tx_response(resp)?; let proposed = Self::extract_proposed_fees(&tx_envelope); let charged = Self::extract_charged_fees(&tx_meta, &tx_result); @@ -266,7 +266,7 @@ impl FeeTable { self.proposed.fee - self.charged.fee } - fn table(&self) -> Table { + pub fn table(&self) -> Table { let mut table = Table::new(); table.set_format(Self::table_format()); @@ -351,7 +351,7 @@ impl FeeTable { table } - fn print(&self) { + pub fn print(&self) { self.table().printstd(); } diff --git a/cmd/soroban-cli/src/commands/tx/fetch/mod.rs b/cmd/soroban-cli/src/commands/tx/fetch/mod.rs index a27aa38063..7f6257c4f6 100644 --- a/cmd/soroban-cli/src/commands/tx/fetch/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/fetch/mod.rs @@ -53,18 +53,26 @@ struct DefaultArgs { pub enum Error { #[error(transparent)] Args(#[from] args::Error), + #[error(transparent)] Result(#[from] result::Error), + #[error(transparent)] Meta(#[from] meta::Error), + #[error(transparent)] Envelope(#[from] envelope::Error), + #[error(transparent)] Events(#[from] events::Error), #[error(transparent)] NotSupported(#[from] fee::Error), + #[error("the following required argument was not provided: {0}")] MissingArg(String), + + #[error(transparent)] + Io(#[from] std::io::Error), } impl Cmd { diff --git a/cmd/soroban-cli/src/commands/tx/simulate.rs b/cmd/soroban-cli/src/commands/tx/simulate.rs index fe599b2f78..7fc1de7833 100644 --- a/cmd/soroban-cli/src/commands/tx/simulate.rs +++ b/cmd/soroban-cli/src/commands/tx/simulate.rs @@ -29,8 +29,10 @@ pub struct Cmd { /// Base-64 transaction envelope XDR or file containing XDR to decode, or stdin if empty #[arg()] pub tx_xdr: Option, + #[clap(flatten)] pub config: config::Args, + /// Allow this many extra instructions when budgeting resources during transaction simulation #[arg(long)] pub instruction_leeway: Option, diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index 2ff7c11197..5ef11e42e4 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -1,9 +1,13 @@ +use std::io::stderr; + use clap::arg; +use soroban_rpc::GetTransactionResponse; use crate::assembled::Assembled; -use crate::xdr; - +use crate::commands::tx::fetch; +use crate::commands::tx::fetch::fee::FeeTable; use crate::commands::HEADING_RPC; +use crate::xdr; #[derive(Debug, clap::Args, Clone)] #[group(skip)] @@ -38,6 +42,18 @@ impl Args { self.instruction_leeway .map(|instruction_leeway| soroban_rpc::ResourceConfig { instruction_leeway }) } + + pub fn print_cost_info(&self, res: &GetTransactionResponse) -> Result<(), fetch::Error> { + if !self.cost { + return Ok(()); + } + + let fee_table = FeeTable::new_from_transaction_response(res)?; + + fee_table.table().print(&mut stderr())?; + + Ok(()) + } } pub fn add_padding_to_instructions(txn: Assembled) -> Assembled {