diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index e4d7410ce..835d2066b 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 be03afa8d..758c6fce0 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 41e7367dc..65557eda9 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -14,7 +14,9 @@ use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; use crate::{ commands::{ config::{self, data}, - global, network, NetworkRunnable, + global, network, + txn_result::TxnResult, + NetworkRunnable, }, rpc::{Client, Error as SorobanRpcError}, utils::{contract_id_hash_from_asset, parsing::parse_asset}, @@ -80,7 +82,7 @@ impl NetworkRunnable for Cmd { &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); // Parse asset let asset = parse_asset(&self.asset)?; @@ -108,8 +110,11 @@ impl NetworkRunnable for Cmd { network_passphrase, &key, )?; + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx)?); + } let txn = client.create_assembled_transaction(&tx).await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = self.fee.apply_to_assembled_txn(txn)?; let get_txn_resp = client .send_assembled_transaction(txn, &key, &[], network_passphrase, None, None) .await? @@ -118,7 +123,9 @@ impl NetworkRunnable for Cmd { data::write(get_txn_resp, &network.rpc_uri()?)?; } - Ok(stellar_strkey::Contract(contract_id.0).to_string()) + Ok(TxnResult::Xdr( + stellar_strkey::Contract(contract_id.0).to_string(), + )) } } diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 40369b230..7a2b929d4 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -17,7 +17,9 @@ use soroban_env_host::{ use crate::commands::{ config::data, contract::{self, id::wasm::get_contract_id}, - global, network, NetworkRunnable, + global, network, + txn_result::{self, TxnResult}, + NetworkRunnable, }; use crate::{ commands::{config, contract::install, HEADING_RPC}, @@ -93,6 +95,8 @@ pub enum Error { #[error(transparent)] WasmId(#[from] contract::id::wasm::Error), #[error(transparent)] + TxnResult(#[from] txn_result::Error), + #[error(transparent)] Data(#[from] data::Error), #[error(transparent)] Network(#[from] network::Error), @@ -115,18 +119,20 @@ impl NetworkRunnable for Cmd { &self, global_args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let wasm_hash = if let Some(wasm) = &self.wasm { + let mut fee = self.fee.clone(); + fee.build_only = false; let hash = install::Cmd { wasm: wasm::Args { wasm: wasm.clone() }, config: config.clone(), - fee: self.fee.clone(), + fee, ignore_checks: self.ignore_checks, } .run_against_rpc_server(global_args, Some(config)) .await?; - hex::encode(hash) + hex::encode(hash.try_res()?) } else { self.wasm_hash .as_ref() @@ -169,8 +175,12 @@ impl NetworkRunnable for Cmd { salt, &key, )?; + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&txn)?); + } + let txn = client.create_assembled_transaction(&txn).await?; - let txn = self.fee.apply_to_assembled_txn(txn); + let txn = self.fee.apply_to_assembled_txn(txn)?; let get_txn_resp = client .send_assembled_transaction(txn, &key, &[], &network.network_passphrase, None, None) .await? @@ -178,7 +188,9 @@ impl NetworkRunnable for Cmd { if global_args.map_or(true, |a| !a.no_cache) { data::write(get_txn_resp, &network.rpc_uri()?)?; } - Ok(stellar_strkey::Contract(contract_id.0).to_string()) + Ok(TxnResult::Res( + stellar_strkey::Contract(contract_id.0).to_string(), + )) } } diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index ab7834959..cebddd0c0 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -11,7 +11,9 @@ use soroban_env_host::xdr::{ use crate::{ commands::{ config::{self, data}, - global, network, NetworkRunnable, + global, network, + txn_result::TxnResult, + NetworkRunnable, }, key, rpc::{self, Client}, @@ -87,7 +89,11 @@ pub enum Error { impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let ttl_ledger = self.run_against_rpc_server(None, None).await?; + let res = self.run_against_rpc_server(None, None).await?; + let TxnResult::Res(ttl_ledger) = &res else { + println!("{}", res.xdr().unwrap()); + return Ok(()); + }; if self.ttl_ledger_only { println!("{ttl_ledger}"); } else { @@ -117,7 +123,7 @@ impl NetworkRunnable for Cmd { &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Self::Error> { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); @@ -161,7 +167,9 @@ impl NetworkRunnable for Cmd { resource_fee: 0, }), }; - + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx)?); + } let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; @@ -194,7 +202,7 @@ impl NetworkRunnable for Cmd { let entry = client.get_full_ledger_entries(&keys).await?; let extension = entry.entries[0].live_until_ledger_seq; if entry.latest_ledger + i64::from(extend_to) < i64::from(extension) { - return Ok(extension); + return Ok(TxnResult::Res(extension)); } } @@ -209,7 +217,7 @@ impl NetworkRunnable for Cmd { }), .. }), - ) => Ok(*live_until_ledger_seq), + ) => Ok(TxnResult::Res(*live_until_ledger_seq)), _ => Err(Error::LedgerEntryNotFound), } } diff --git a/cmd/soroban-cli/src/commands/contract/fetch.rs b/cmd/soroban-cli/src/commands/contract/fetch.rs index eefb1b4b8..5b223e71a 100644 --- a/cmd/soroban-cli/src/commands/contract/fetch.rs +++ b/cmd/soroban-cli/src/commands/contract/fetch.rs @@ -21,6 +21,7 @@ use stellar_strkey::DecodeError; use super::super::config::{self, locator}; use crate::commands::network::{self, Network}; +use crate::commands::txn_result::TxnResult; use crate::commands::{global, NetworkRunnable}; use crate::{ rpc::{self, Client}, @@ -116,7 +117,14 @@ impl Cmd { } pub async fn get_bytes(&self) -> Result, Error> { - self.run_against_rpc_server(None, None).await + // This is safe because fetch doesn't create a transaction + unsafe { + Ok(self + .run_against_rpc_server(None, None) + .await? + .res() + .unwrap_unchecked()) + } } pub fn network(&self) -> Result { @@ -137,7 +145,7 @@ impl NetworkRunnable for Cmd { &self, _args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result, Error> { + ) -> Result>, Error> { let network = config.map_or_else(|| self.network(), |c| Ok(c.get_network()?))?; tracing::trace!(?network); let contract_id = self.contract_id()?; @@ -146,7 +154,7 @@ impl NetworkRunnable for Cmd { .verify_network_passphrase(Some(&network.network_passphrase)) .await?; // async closures are not yet stable - Ok(client.get_remote_wasm(&contract_id).await?) + Ok(TxnResult::Res(client.get_remote_wasm(&contract_id).await?)) } } pub fn get_contract_wasm_from_storage( diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 8763fd7e6..7208e1a93 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -12,6 +12,7 @@ use soroban_env_host::xdr::{ use super::restore; use crate::commands::network; +use crate::commands::txn_result::TxnResult; use crate::commands::{config::data, global, NetworkRunnable}; use crate::key; use crate::rpc::{self, Client}; @@ -72,7 +73,10 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let res_str = hex::encode(self.run_against_rpc_server(None, None).await?); + let res_str = match self.run_against_rpc_server(None, None).await? { + TxnResult::Xdr(xdr) => xdr, + TxnResult::Res(hash) => hex::encode(hash), + }; println!("{res_str}"); Ok(()) } @@ -86,7 +90,7 @@ impl NetworkRunnable for Cmd { &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let contract = self.wasm.read()?; let network = config.get_network()?; @@ -125,6 +129,9 @@ impl NetworkRunnable for Cmd { let (tx_without_preflight, hash) = build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &key)?; + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx_without_preflight)?); + } let code_key = xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() }); let contract_data = client.get_ledger_entries(&[code_key]).await?; @@ -142,7 +149,7 @@ impl NetworkRunnable for Cmd { // Skip reupload if this isn't V0 because V1 extension already // exists. if code.ext.ne(&ContractCodeEntryExt::V0) { - return Ok(hash); + return Ok(TxnResult::Res(hash)); } } _ => { @@ -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 77bf28603..9a1acf5a7 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -12,15 +12,16 @@ use heck::ToKebabCase; use soroban_env_host::{ xdr::{ - self, ContractDataEntry, Error as XdrError, Hash, HostFunction, InvokeContractArgs, - InvokeHostFunctionOp, LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, - OperationBody, Preconditions, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, - ScVal, ScVec, SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, + self, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, LedgerEntryData, + LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, Preconditions, PublicKey, + ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, SequenceNumber, + SorobanAuthorizationEntry, SorobanResources, String32, StringM, Transaction, TransactionExt, Uint256, VecM, }, HostError, }; +use soroban_sdk::xdr::{AccountEntry, AccountEntryExt, AccountId, ContractDataEntry, DiagnosticEvent, Thresholds}; use soroban_spec::read::FromWasmError; use stellar_strkey::DecodeError; @@ -28,6 +29,7 @@ use super::super::{ config::{self, locator}, events, }; +use crate::commands::txn_result::TxnResult; use crate::commands::NetworkRunnable; use crate::{ commands::{config::data, global, network}, @@ -80,7 +82,7 @@ pub enum Error { error: soroban_spec_tools::Error, }, #[error("cannot add contract to ledger entries: {0}")] - CannotAddContractToLedgerEntries(XdrError), + CannotAddContractToLedgerEntries(xdr::Error), #[error(transparent)] // TODO: the Display impl of host errors is pretty user-unfriendly // (it just calls Debug). I think we can do better than that @@ -109,7 +111,7 @@ pub enum Error { error: soroban_spec_tools::Error, }, #[error(transparent)] - Xdr(#[from] XdrError), + Xdr(#[from] xdr::Error), #[error("error parsing int: {0}")] ParseIntError(#[from] ParseIntError), #[error(transparent)] @@ -169,6 +171,7 @@ impl Cmd { &self, contract_id: [u8; 32], spec_entries: &[ScSpecEntry], + config: &config::Args, ) -> Result<(String, Spec, InvokeContractArgs, Vec), Error> { let spec = Spec(Some(spec_entries.to_vec())); let mut cmd = clap::Command::new(self.contract_id.clone()) @@ -201,7 +204,7 @@ impl Cmd { let cmd = crate::commands::keys::address::Cmd { name: s.clone(), hd_path: Some(0), - locator: self.config.locator.clone(), + locator: config.locator.clone(), }; if let Ok(address) = cmd.public_key() { s = address.to_string(); @@ -272,7 +275,7 @@ impl Cmd { Ok(()) } - pub async fn invoke(&self, global_args: &global::Args) -> Result { + pub async fn invoke(&self, global_args: &global::Args) -> Result, Error> { self.run_against_rpc_server(Some(global_args), None).await } @@ -311,7 +314,7 @@ impl NetworkRunnable for Cmd { &self, global_args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); @@ -319,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:?}"); @@ -363,15 +371,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()?)?; @@ -388,7 +399,7 @@ impl NetworkRunnable for Cmd { let res = client .send_assembled_transaction( txn, - &key, + &config.key_pair()?, &signers, &network.network_passphrase, Some(log_events), @@ -406,10 +417,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); @@ -420,7 +448,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 @@ -431,14 +463,14 @@ pub fn output_to_string(spec: &Spec, res: &ScVal, function: &str) -> Result Result { let op = Operation { source_account: None, @@ -448,7 +480,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, @@ -508,16 +540,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..1d23e5c6a 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -14,7 +14,7 @@ use soroban_env_host::{ use soroban_sdk::xdr::Limits; use crate::{ - commands::{config, global, NetworkRunnable}, + commands::{config, global, txn_result::TxnResult, NetworkRunnable}, key, rpc::{self, Client, FullLedgerEntries, FullLedgerEntry}, }; @@ -91,7 +91,13 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let entries = self.run_against_rpc_server(None, None).await?; + let entries = match self.run_against_rpc_server(None, None).await? { + TxnResult::Res(res) => res, + TxnResult::Xdr(xdr) => { + println!("{xdr}"); + return Ok(()); + } + }; self.output_entries(&entries) } @@ -178,12 +184,12 @@ impl NetworkRunnable for Cmd { &self, _: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); let client = Client::new(&network.rpc_url)?; let keys = self.key.parse_keys()?; - Ok(client.get_full_ledger_entries(&keys).await?) + Ok(TxnResult::Res(client.get_full_ledger_entries(&keys).await?)) } } diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 8b5921a1a..6fc8eb17f 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -13,7 +13,9 @@ use crate::{ commands::{ config::{self, data, locator}, contract::extend, - global, network, NetworkRunnable, + global, network, + txn_result::{self, TxnResult}, + NetworkRunnable, }, key, rpc::{self, Client}, @@ -87,13 +89,21 @@ pub enum Error { Data(#[from] data::Error), #[error(transparent)] Network(#[from] network::Error), + + #[error(transparent)] + TxnResult(#[from] txn_result::Error), } impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let expiration_ledger_seq = self.run_against_rpc_server(None, None).await?; - + let expiration_ledger_seq = match self.run_against_rpc_server(None, None).await? { + TxnResult::Res(res) => res, + TxnResult::Xdr(xdr) => { + println!("{xdr}"); + return Ok(()); + } + }; if let Some(ledgers_to_extend) = self.ledgers_to_extend { extend::Cmd { key: self.key.clone(), @@ -121,7 +131,7 @@ impl NetworkRunnable for Cmd { &self, args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let config = config.unwrap_or(&self.config); let network = config.get_network()?; tracing::trace!(?network); @@ -162,7 +172,9 @@ impl NetworkRunnable for Cmd { resource_fee: 0, }), }; - + if self.fee.build_only { + return Ok(TxnResult::from_xdr(&tx)?); + } let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; @@ -198,7 +210,9 @@ impl NetworkRunnable for Cmd { operations[0].changes.len() ); } - parse_operations(operations).ok_or(Error::MissingOperationResult) + Ok(TxnResult::Res( + parse_operations(operations).ok_or(Error::MissingOperationResult)?, + )) } } diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index 42145f5bf..f1707f349 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -5,7 +5,9 @@ use soroban_env_host::xdr::{self, Limits, ReadXdr}; use super::{ config::{self, locator}, - global, network, NetworkRunnable, + global, network, + txn_result::TxnResult, + NetworkRunnable, }; use crate::{rpc, utils}; @@ -124,6 +126,8 @@ pub enum Error { Locator(#[from] locator::Error), #[error(transparent)] Config(#[from] config::Error), + #[error(transparent)] + TxnResult(#[from] super::txn_result::Error), } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] @@ -167,7 +171,8 @@ impl Cmd { })?; } - let response = self.run_against_rpc_server(None, None).await?; + let txn_res = self.run_against_rpc_server(None, None).await?; + let response = txn_res.try_res()?; for event in &response.events { match self.output { @@ -214,7 +219,7 @@ impl NetworkRunnable for Cmd { &self, _args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result { + ) -> Result, Error> { let start = self.start()?; let network = if let Some(config) = config { Ok(config.get_network()?) @@ -226,15 +231,17 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - client - .get_events( - start, - Some(self.event_type), - &self.contract_ids, - &self.topic_filters, - Some(self.count), - ) - .await - .map_err(Error::Rpc) + Ok(TxnResult::Res( + client + .get_events( + start, + Some(self.event_type), + &self.contract_ids, + &self.topic_filters, + Some(self.count), + ) + .await + .map_err(Error::Rpc)?, + )) } } diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 4bb5dcb53..df28339bd 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -12,7 +12,9 @@ pub mod global; pub mod keys; pub mod network; pub mod plugin; +pub mod txn; 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. @@ -99,6 +101,7 @@ impl Root { Cmd::Network(network) => network.run().await?, Cmd::Version(version) => version.run(), Cmd::Keys(id) => id.run().await?, + Cmd::Txn(tx) => tx.run().await?, Cmd::Cache(data) => data.run()?, }; Ok(()) @@ -133,6 +136,9 @@ pub enum Cmd { Network(network::Cmd), /// Print version information Version(version::Cmd), + /// Sign, Simulate, and Send transactions + #[command(subcommand)] + Txn(txn::Cmd), /// Cache for tranasctions and contract specs #[command(subcommand)] Cache(cache::Cmd), @@ -156,6 +162,8 @@ pub enum Error { #[error(transparent)] Network(#[from] network::Error), #[error(transparent)] + Txn(#[from] txn::Error), + #[error(transparent)] Cache(#[from] cache::Error), } @@ -168,5 +176,5 @@ pub trait NetworkRunnable { &self, global_args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result; + ) -> Result, Self::Error>; } diff --git a/cmd/soroban-cli/src/commands/txn/mod.rs b/cmd/soroban-cli/src/commands/txn/mod.rs new file mode 100644 index 000000000..9de360fcc --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn/mod.rs @@ -0,0 +1,48 @@ +use clap::Parser; + +pub mod send; +pub mod sign; +pub mod simulate; +pub mod xdr; + +use stellar_xdr::cli as xdr_cli; + +#[derive(Debug, Parser)] +pub enum Cmd { + /// Add a new identity (keypair, ledger, macOS keychain) + Inspect(xdr_cli::Root), + /// Given an identity return its address (public key) + Sign(sign::Cmd), + /// Submit a transaction to the network + Send(send::Cmd), + /// Simulate a transaction + Simulate(simulate::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// An error during the simulation + #[error(transparent)] + Simulate(#[from] simulate::Error), + /// An error during the inspect + #[error(transparent)] + Inspect(#[from] xdr_cli::Error), + /// An error during the sign + #[error(transparent)] + Sign(#[from] sign::Error), + /// An error during the send + #[error(transparent)] + Send(#[from] send::Error), +} + +impl Cmd { + pub async fn run(&self) -> Result<(), Error> { + match self { + Cmd::Inspect(cmd) => cmd.run()?, + Cmd::Sign(cmd) => cmd.run().await?, + Cmd::Send(cmd) => cmd.run().await?, + Cmd::Simulate(cmd) => cmd.run().await?, + }; + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/txn/send.rs b/cmd/soroban-cli/src/commands/txn/send.rs new file mode 100644 index 000000000..1cf3f8478 --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn/send.rs @@ -0,0 +1,35 @@ +use soroban_rpc::GetTransactionResponse; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + XdrArgs(#[from] super::xdr::Error), + #[error(transparent)] + Config(#[from] super::super::config::Error), + #[error(transparent)] + Rpc(#[from] crate::rpc::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + #[clap(flatten)] + pub xdr_args: super::xdr::Args, + #[clap(flatten)] + pub config: super::super::config::Args, +} + +impl Cmd { + pub async fn run(&self) -> Result<(), Error> { + let response = self.send().await?; + println!("{response:#?}"); + Ok(()) + } + + pub async fn send(&self) -> Result { + let txn_env = self.xdr_args.txn_envelope()?; + let network = self.config.get_network()?; + let client = crate::rpc::Client::new(&network.rpc_url)?; + Ok(client.send_transaction(&txn_env).await?) + } +} diff --git a/cmd/soroban-cli/src/commands/txn/sign.rs b/cmd/soroban-cli/src/commands/txn/sign.rs new file mode 100644 index 000000000..ec4bb1155 --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn/sign.rs @@ -0,0 +1,97 @@ +use std::io; + +// use crossterm::{ +// event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, +// execute, +// terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, +// }; +use soroban_sdk::xdr::{self, Limits, TransactionEnvelope, WriteXdr}; + +use crate::signer::{self, InMemory, Stellar}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + XdrArgs(#[from] super::xdr::Error), + #[error(transparent)] + Signer(#[from] signer::Error), + #[error(transparent)] + Config(#[from] super::super::config::Error), + #[error(transparent)] + StellarStrkey(#[from] stellar_strkey::DecodeError), + #[error(transparent)] + Xdr(#[from] xdr::Error), + #[error(transparent)] + Io(#[from] io::Error), + #[error("User cancelled signing, perhaps need to add -y")] + UserCancelledSigning, +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + /// Confirm that a signature can be signed by the given keypair automatically. + #[arg(long, short = 'y')] + yes: bool, + #[clap(flatten)] + pub xdr_args: super::xdr::Args, + #[clap(flatten)] + pub config: super::super::config::Args, +} + +impl Cmd { + #[allow(clippy::unused_async)] + pub async fn run(&self) -> Result<(), Error> { + let envelope = self.sign()?; + println!("{}", envelope.to_xdr_base64(Limits::none())?.trim()); + Ok(()) + } + + pub fn sign(&self) -> Result { + let source = &self.config.source_account; + tracing::debug!("signing transaction with source account {}", source); + let txn = self.xdr_args.txn()?; + let key = self.config.key_pair()?; + let address = + stellar_strkey::ed25519::PublicKey::from_payload(key.verifying_key().as_bytes())?; + let in_memory = InMemory { + network_passphrase: self.config.get_network()?.network_passphrase, + keypairs: vec![key], + }; + self.prompt_user()?; + Ok(in_memory.sign_txn(txn, &stellar_strkey::Strkey::PublicKeyEd25519(address))?) + } + + pub fn prompt_user(&self) -> Result<(), Error> { + if self.yes { + return Ok(()); + } + Err(Error::UserCancelledSigning) + // TODO use crossterm to prompt user for confirmation + // // Set up the terminal + // let mut stdout = io::stdout(); + // execute!(stdout, EnterAlternateScreen)?; + // terminal::enable_raw_mode()?; + + // println!("Press 'y' or 'Y' for yes, any other key for no:"); + + // loop { + // if let Event::Key(KeyEvent { + // code, + // modifiers: KeyModifiers::NONE, + // .. + // }) = event::read()? + // { + // match code { + // KeyCode::Char('y' | 'Y') => break, + // _ => return Err(Error::UserCancelledSigning), + // } + // } + // } + + // // Clean up the terminal + // terminal::disable_raw_mode()?; + // execute!(stdout, LeaveAlternateScreen)?; + // Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/txn/simulate.rs b/cmd/soroban-cli/src/commands/txn/simulate.rs new file mode 100644 index 000000000..5fbae28c0 --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn/simulate.rs @@ -0,0 +1,38 @@ +use soroban_rpc::Assembled; +use soroban_sdk::xdr::{self, WriteXdr}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + XdrArgs(#[from] super::xdr::Error), + #[error(transparent)] + Config(#[from] super::super::config::Error), + #[error(transparent)] + Rpc(#[from] crate::rpc::Error), + #[error(transparent)] + Xdr(#[from] xdr::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + #[clap(flatten)] + pub xdr_args: super::xdr::Args, + #[clap(flatten)] + pub config: super::super::config::Args, +} + +impl Cmd { + pub async fn run(&self) -> Result<(), Error> { + let res = self.simulate().await?; + println!("{}", res.transaction().to_xdr_base64(xdr::Limits::none())?); + Ok(()) + } + + pub async fn simulate(&self) -> Result { + let tx = self.xdr_args.txn()?; + let network = self.config.get_network()?; + let client = crate::rpc::Client::new(&network.rpc_url)?; + Ok(client.create_assembled_transaction(&tx).await?) + } +} diff --git a/cmd/soroban-cli/src/commands/txn/xdr.rs b/cmd/soroban-cli/src/commands/txn/xdr.rs new file mode 100644 index 000000000..180c12e6b --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn/xdr.rs @@ -0,0 +1,67 @@ +use std::{ + io::{stdin, Read}, + path::PathBuf, +}; + +use soroban_env_host::xdr::ReadXdr; +use soroban_sdk::xdr::{Limits, Transaction, TransactionEnvelope}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("failed to decode XDR from base64")] + Base64Decode, + #[error("failed to decode XDR from file: {0}")] + FileDecode(PathBuf), + #[error("failed to decode XDR from stdin")] + StdinDecode, + #[error(transparent)] + Io(#[from] std::io::Error), +} + +/// XDR input, either base64 encoded or file path and stdin if neither is provided +#[derive(Debug, clap::Args, Clone)] +#[group(skip)] +pub struct Args { + /// Base64 encoded XDR transaction + #[arg( + long = "xdr-base64", + env = "STELLAR_TXN_XDR_BASE64", + conflicts_with = "xdr_file" + )] + pub xdr_base64: Option, + //// File containing Binary encoded data + #[arg( + long = "xdr-file", + env = "STELLAR_TXN_XDR_FILE", + conflicts_with = "xdr_base64" + )] + pub xdr_file: Option, +} + +impl Args { + pub fn xdr(&self) -> Result { + match (self.xdr_base64.as_ref(), self.xdr_file.as_ref()) { + (Some(xdr_base64), None) => { + T::from_xdr_base64(xdr_base64, Limits::none()).map_err(|_| Error::Base64Decode) + } + (_, Some(xdr_file)) => T::from_xdr(std::fs::read(xdr_file)?, Limits::none()) + .map_err(|_| Error::FileDecode(xdr_file.clone())), + + _ => { + let mut buf = String::new(); + let _ = stdin() + .read_to_string(&mut buf) + .map_err(|_| Error::StdinDecode)?; + T::from_xdr_base64(buf.trim(), Limits::none()).map_err(|_| Error::StdinDecode) + } + } + } + + pub fn txn(&self) -> Result { + self.xdr::() + } + + pub fn txn_envelope(&self) -> Result { + self.xdr::() + } +} 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..f69f549be --- /dev/null +++ b/cmd/soroban-cli/src/commands/txn_result.rs @@ -0,0 +1,56 @@ +use std::fmt::{Display, Formatter}; + +use soroban_sdk::xdr::{self, Limits, WriteXdr}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Expect xdr string")] + XdrStringExpected, + #[error("Expect result")] + ResultExpected, +} + +pub enum TxnResult { + Xdr(String), + Res(T), +} + +impl TxnResult { + pub fn from_xdr(res: &impl WriteXdr) -> Result { + Ok(TxnResult::Xdr(res.to_xdr_base64(Limits::none())?)) + } + + pub fn xdr(&self) -> Option<&str> { + match self { + TxnResult::Xdr(xdr) => Some(xdr), + TxnResult::Res(_) => None, + } + } + + pub fn res(self) -> Option { + match self { + TxnResult::Res(res) => Some(res), + TxnResult::Xdr(_) => None, + } + } + + pub fn try_xdr(&self) -> Result<&str, Error> { + self.xdr().ok_or(Error::XdrStringExpected) + } + + pub fn try_res(self) -> Result { + self.res().ok_or(Error::ResultExpected) + } +} + +impl Display for TxnResult +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TxnResult::Xdr(xdr) => write!(f, "{xdr}"), + TxnResult::Res(res) => write!(f, "{res}"), + } + } +} diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index 353fe6e5d..f6ac76d58 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -1,5 +1,6 @@ use clap::arg; -use soroban_env_host::xdr; + +use soroban_env_host::xdr::{self, WriteXdr}; use soroban_rpc::Assembled; use crate::commands::HEADING_RPC; @@ -16,15 +17,31 @@ pub struct Args { /// Number of instructions to simulate #[arg(long, help_heading = HEADING_RPC)] pub instructions: Option, + /// Build the transaction only write the base64 xdr to stdout + #[arg(long, help_heading = HEADING_RPC)] + pub build_only: bool, + /// Simulation the transaction only write the base64 xdr to stdout + #[arg(long, help_heading = HEADING_RPC, conflicts_with = "build_only")] + pub sim_only: bool, } impl Args { - pub fn apply_to_assembled_txn(&self, txn: Assembled) -> Assembled { - if let Some(instructions) = self.instructions { + pub fn apply_to_assembled_txn(&self, txn: Assembled) -> Result { + let simulated_txn = if let Some(instructions) = self.instructions { txn.set_max_instructions(instructions) } else { add_padding_to_instructions(txn) + }; + if self.sim_only { + println!( + "{}", + simulated_txn + .transaction() + .to_xdr_base64(xdr::Limits::none())? + ); + std::process::exit(0); } + Ok(simulated_txn) } } @@ -47,6 +64,8 @@ impl Default for Args { fee: 100, cost: false, instructions: None, + build_only: false, + sim_only: false, } } } diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index d4118a6be..9a9900605 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -3,15 +3,18 @@ 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; pub mod key; pub mod log; pub mod toid; +pub mod signer; pub mod utils; pub mod wasm; diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs new file mode 100644 index 000000000..bd5605598 --- /dev/null +++ b/cmd/soroban-cli/src/signer.rs @@ -0,0 +1,277 @@ +use ed25519_dalek::Signer; +use sha2::{Digest, Sha256}; + +use soroban_env_host::xdr::{ + self, AccountId, DecoratedSignature, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, + InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ScAddress, ScMap, ScSymbol, + ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, + SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, + TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + TransactionV1Envelope, Uint256, WriteXdr, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Xdr(#[from] xdr::Error), + #[error("Error signing transaction {address}")] + MissingSignerForAddress { address: String }, +} + +fn requires_auth(txn: &Transaction) -> Option { + let [op @ Operation { + body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), + .. + }] = txn.operations.as_slice() + else { + return None; + }; + matches!( + auth.first().map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + ) + .then(move || op.clone()) +} + +/// A trait for signing Stellar transactions and Soroban authorization entries +pub trait Stellar { + /// The type of the options that can be passed when creating a new signer + type Init; + /// Create a new signer with the given network passphrase and options + fn new(network_passphrase: &str, options: Option) -> Self; + + /// Get the network hash + fn network_hash(&self) -> xdr::Hash; + + /// Sign a transaction hash with the given source account + /// # Errors + /// Returns an error if the source account is not found + fn sign_txn_hash( + &self, + txn: [u8; 32], + source_account: &stellar_strkey::Strkey, + ) -> Result; + + /// Sign a Soroban authorization entry with the given address + /// # Errors + /// Returns an error if the address is not found + fn sign_soroban_authorization_entry( + &self, + unsigned_entry: &SorobanAuthorizationEntry, + signature_expiration_ledger: u32, + address: &[u8; 32], + ) -> Result; + + /// Sign a Stellar transaction with the given source account + /// This is a default implementation that signs the transaction hash and returns a decorated signature + /// # Errors + /// Returns an error if the source account is not found + fn sign_txn( + &self, + txn: Transaction, + source_account: &stellar_strkey::Strkey, + ) -> Result { + let signature_payload = TransactionSignaturePayload { + network_id: self.network_hash(), + tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(txn.clone()), + }; + let hash = Sha256::digest(signature_payload.to_xdr(Limits::none())?).into(); + let decorated_signature = self.sign_txn_hash(hash, source_account)?; + Ok(TransactionEnvelope::Tx(TransactionV1Envelope { + tx: txn, + signatures: vec![decorated_signature].try_into()?, + })) + } + + /// Sign a Soroban authorization entries for a given transaction and set the expiration ledger + /// # Errors + /// Returns an error if the address is not found + fn sign_soroban_authorizations( + &self, + raw: &Transaction, + signature_expiration_ledger: u32, + ) -> Result, Error> { + let mut tx = raw.clone(); + let Some(mut op) = requires_auth(&tx) else { + return Ok(None); + }; + + let xdr::Operation { + body: OperationBody::InvokeHostFunction(ref mut body), + .. + } = op + else { + return Ok(None); + }; + + let signed_auths = body + .auth + .as_slice() + .iter() + .map(|raw_auth| { + self.maybe_sign_soroban_authorization_entry(raw_auth, signature_expiration_ledger) + }) + .collect::, Error>>()?; + + body.auth = signed_auths.try_into()?; + tx.operations = vec![op].try_into()?; + Ok(Some(tx)) + } + + /// Sign a Soroban authorization entry if the address is public key + /// # Errors + /// Returns an error if the address in entry is a contract + fn maybe_sign_soroban_authorization_entry( + &self, + unsigned_entry: &SorobanAuthorizationEntry, + signature_expiration_ledger: u32, + ) -> Result { + if let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { ref address, .. }), + .. + } = unsigned_entry + { + // See if we have a signer for this authorizationEntry + // If not, then we Error + let needle = match address { + ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a, + ScAddress::Contract(Hash(c)) => { + // This address is for a contract. This means we're using a custom + // smart-contract account. Currently the CLI doesn't support that yet. + return Err(Error::MissingSignerForAddress { + address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c)) + .to_string(), + }); + } + }; + self.sign_soroban_authorization_entry( + unsigned_entry, + signature_expiration_ledger, + needle, + ) + } else { + Ok(unsigned_entry.clone()) + } + } +} + +use std::fmt::Debug; +#[derive(Debug)] +pub struct InMemory { + pub network_passphrase: String, + pub keypairs: Vec, +} + +impl InMemory { + pub fn get_key( + &self, + key: &stellar_strkey::Strkey, + ) -> Result<&ed25519_dalek::SigningKey, Error> { + match key { + stellar_strkey::Strkey::PublicKeyEd25519(stellar_strkey::ed25519::PublicKey(bytes)) => { + self.keypairs + .iter() + .find(|k| k.verifying_key().to_bytes() == *bytes) + } + _ => None, + } + .ok_or_else(|| Error::MissingSignerForAddress { + address: key.to_string(), + }) + } +} + +impl Stellar for InMemory { + type Init = Vec; + fn new(network_passphrase: &str, options: Option>) -> Self { + InMemory { + network_passphrase: network_passphrase.to_string(), + keypairs: options.unwrap_or_default(), + } + } + + fn sign_txn_hash( + &self, + txn: [u8; 32], + source_account: &stellar_strkey::Strkey, + ) -> Result { + let source_account = self.get_key(source_account)?; + let tx_signature = source_account.sign(&txn); + Ok(DecoratedSignature { + // TODO: remove this unwrap. It's safe because we know the length of the array + hint: SignatureHint( + source_account.verifying_key().to_bytes()[28..] + .try_into() + .unwrap(), + ), + signature: Signature(tx_signature.to_bytes().try_into()?), + }) + } + + fn sign_soroban_authorization_entry( + &self, + unsigned_entry: &SorobanAuthorizationEntry, + signature_expiration_ledger: u32, + signer: &[u8; 32], + ) -> Result { + let mut auth = unsigned_entry.clone(); + let SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(ref mut credentials), + .. + } = auth + else { + // Doesn't need special signing + return Ok(auth); + }; + let SorobanAddressCredentials { nonce, .. } = credentials; + + let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { + network_id: self.network_hash(), + invocation: auth.root_invocation.clone(), + nonce: *nonce, + signature_expiration_ledger, + }) + .to_xdr(Limits::none())?; + + let strkey = stellar_strkey::ed25519::PublicKey(*signer); + let payload = Sha256::digest(preimage); + let signer = self.get_key(&stellar_strkey::Strkey::PublicKeyEd25519(strkey))?; + let signature = signer.sign(&payload); + + let map = ScMap::sorted_from(vec![ + ( + ScVal::Symbol(ScSymbol("public_key".try_into()?)), + ScVal::Bytes( + signer + .verifying_key() + .to_bytes() + .to_vec() + .try_into() + .map_err(Error::Xdr)?, + ), + ), + ( + ScVal::Symbol(ScSymbol("signature".try_into()?)), + ScVal::Bytes( + signature + .to_bytes() + .to_vec() + .try_into() + .map_err(Error::Xdr)?, + ), + ), + ]) + .map_err(Error::Xdr)?; + credentials.signature = ScVal::Vec(Some( + vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?, + )); + credentials.signature_expiration_ledger = signature_expiration_ledger; + auth.credentials = SorobanCredentials::Address(credentials.clone()); + + Ok(auth) + } + + fn network_hash(&self) -> xdr::Hash { + xdr::Hash(Sha256::digest(self.network_passphrase.as_bytes()).into()) + } +} diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index 766a7798e..9d10a653e 100644 --- a/docs/soroban-cli-full-docs.md +++ b/docs/soroban-cli-full-docs.md @@ -240,6 +240,14 @@ Deploy builtin Soroban Asset Contract Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + @@ -391,6 +399,14 @@ If no keys are specified the contract itself is extended. Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + @@ -423,6 +439,14 @@ Deploy a wasm contract Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + * `-i`, `--ignore-checks` — Whether to ignore safety checks when deploying contracts Default value: `false` @@ -587,6 +611,14 @@ Install a WASM file to the ledger without creating a contract instance Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + * `--wasm ` — Path to wasm binary * `-i`, `--ignore-checks` — Whether to ignore safety checks when deploying contracts @@ -636,6 +668,14 @@ soroban contract invoke ... -- --help Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + @@ -748,6 +788,14 @@ If no keys are specificed the contract itself is restored. Possible values: `true`, `false` * `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + +* `--sim-only` — Simulation the transaction only write the base64 xdr to stdout + + Possible values: `true`, `false` + @@ -1339,6 +1387,9 @@ Read cached action * `--id ` — ID of the cache entry + Possible values: `envelope` + +