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<String, Error> {
+    ) -> Result<TxnResult<String>, 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<String, Error> {
+    ) -> Result<TxnResult<String>, 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<u32, Self::Error> {
+    ) -> Result<TxnResult<u32>, 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<Vec<u8>, 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<Network, Error> {
@@ -137,7 +145,7 @@ impl NetworkRunnable for Cmd {
         &self,
         _args: Option<&global::Args>,
         config: Option<&config::Args>,
-    ) -> Result<Vec<u8>, Error> {
+    ) -> Result<TxnResult<Vec<u8>>, 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<Hash, Error> {
+    ) -> Result<TxnResult<Hash>, 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<SigningKey>), 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<String, Error> {
+    pub async fn invoke(&self, global_args: &global::Args) -> Result<TxnResult<String>, 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<String, Error> {
+    ) -> Result<TxnResult<String>, 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<SorobanAuthorizationEntry>],
-    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<String, Error> {
+pub fn output_to_string(
+    spec: &Spec,
+    res: &ScVal,
+    function: &str,
+) -> Result<TxnResult<String>, 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<Stri
             })?
             .to_string();
     }
-    Ok(res_str)
+    Ok(TxnResult::Res(res_str))
 }
 
 fn build_invoke_contract_tx(
     parameters: InvokeContractArgs,
     sequence: i64,
     fee: u32,
-    key: &SigningKey,
+    source_account_id: Uint256,
 ) -> Result<Transaction, Error> {
     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<clap::Command, Error> {
 
         // 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<FullLedgerEntries, Error> {
+    ) -> Result<TxnResult<FullLedgerEntries>, 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<u32, Error> {
+    ) -> Result<TxnResult<u32>, 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<rpc::GetEventsResponse, Error> {
+    ) -> Result<TxnResult<rpc::GetEventsResponse>, 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<Self::Result, Self::Error>;
+    ) -> Result<txn_result::TxnResult<Self::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<T> {
+    Xdr(String),
+    Res(T),
+}
+
+impl<T> TxnResult<T> {
+    pub fn from_xdr(res: &impl WriteXdr) -> Result<Self, xdr::Error> {
+        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<T> {
+        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<T, Error> {
+        self.res().ok_or(Error::ResultExpected)
+    }
+}
+
+impl<T> Display for TxnResult<T>
+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<u32>,
+    /// 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<Assembled, xdr::Error> {
+        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 <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 <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 <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 <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 <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 <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 <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>` — ID of the cache entry
 
+  Possible values: `envelope`
+
+
 
 
 <hr/>