From ffbcf0c8a0933ec05946d88c5967f80cb80939f0 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 16 Jan 2024 20:20:40 -0500 Subject: [PATCH] feat: add methods to GetTransactionResponse And split up prepare_and_send_transaction to allow creating an assembled transaction and passing it when sending a transaction. --- Cargo.toml | 24 +++- .../tests/it/integration/hello_world.rs | 5 +- cmd/soroban-cli/Cargo.toml | 14 ++- .../src/commands/contract/extend.rs | 9 +- .../src/commands/contract/install.rs | 14 +-- .../src/commands/contract/invoke.rs | 44 ++++---- cmd/soroban-cli/src/commands/contract/read.rs | 2 +- .../src/commands/contract/restore.rs | 10 +- cmd/soroban-cli/src/rpc/log.rs | 2 + .../src/rpc/log/diagnostic_events.rs | 11 ++ cmd/soroban-cli/src/rpc/mod.rs | 104 ++++++++++++++---- cmd/soroban-cli/src/rpc/txn.rs | 79 ++++++++++--- cmd/soroban-rpc/internal/test/cli_test.go | 2 +- docs/soroban-cli-full-docs.md | 1 + 14 files changed, 236 insertions(+), 85 deletions(-) create mode 100644 cmd/soroban-cli/src/rpc/log.rs create mode 100644 cmd/soroban-cli/src/rpc/log/diagnostic_events.rs diff --git a/Cargo.toml b/Cargo.toml index c88cd4abc7..df5780093f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,22 +74,40 @@ version = "=20.0.2" default-features = true [workspace.dependencies] +stellar-strkey = "0.0.7" +sep5 = "0.0.2" base64 = "0.21.2" thiserror = "1.0.46" sha2 = "0.10.7" ethnum = "1.3.2" hex = "0.4.3" itertools = "0.10.0" -sep5 = "0.0.2" + +serde-aux = "4.1.2" serde_json = "1.0.82" serde = "1.0.82" -stellar-strkey = "0.0.7" + +clap = { version = "4.1.8", features = [ + "derive", + "env", + "deprecated", + "string", +] } +clap_complete = "4.1.4" tracing = "0.1.37" tracing-subscriber = "0.3.16" tracing-appender = "0.2.2" which = "4.4.0" wasmparser = "0.90.0" - +termcolor = "1.1.3" +termcolor_output = "1.0.1" +ed25519-dalek = "2.0.0" + +# networking +http = "1.0.0" +jsonrpsee-http-client = "0.20.1" +jsonrpsee-core = "0.20.1" +tokio = "1.28.1" # [patch."https://github.com/stellar/rs-soroban-env"] # soroban-env-host = { path = "../rs-soroban-env/soroban-env-host/" } 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 7714f70dd4..1a44920d74 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -191,10 +191,9 @@ async fn contract_data_read() { const KEY: &str = "COUNTER"; let sandbox = &TestEnv::default(); let id = &deploy_hello(sandbox); + println!("{id}"); let res = sandbox.invoke(&["--id", id, "--", "inc"]).await.unwrap(); assert_eq!(res.trim(), "1"); - extend(sandbox, id, Some(KEY)).await; - sandbox .new_assert_cmd("contract") .arg("read") @@ -207,6 +206,8 @@ async fn contract_data_read() { .success() .stdout(predicates::str::starts_with("COUNTER,1")); + extend(sandbox, id, Some(KEY)).await; + sandbox .new_assert_cmd("contract") .arg("invoke") diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index ad60c827ac..85f4b84b45 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -45,24 +45,26 @@ soroban-spec-typescript = { workspace = true } soroban-ledger-snapshot = { workspace = true } stellar-strkey = { workspace = true } soroban-sdk = { workspace = true } -clap = { version = "4.1.8", features = [ + +clap = { workspace = true, features = [ "derive", "env", "deprecated", "string", ] } +clap_complete = {workspace = true} + base64 = { workspace = true } thiserror = { workspace = true } serde = "1.0.82" serde_derive = "1.0.82" serde_json = "1.0.82" -serde-aux = "4.1.2" +serde-aux = { workspace = true } hex = { workspace = true } num-bigint = "0.4" tokio = { version = "1", features = ["full"] } -termcolor = "1.1.3" -termcolor_output = "1.0.1" -clap_complete = "4.1.4" +termcolor = { workspace = true } +termcolor_output = { workspace = true } rand = "0.8.5" wasmparser = { workspace = true } sha2 = { workspace = true } @@ -70,7 +72,7 @@ csv = "1.1.6" ed25519-dalek = "=2.0.0" jsonrpsee-http-client = "0.20.1" jsonrpsee-core = "0.20.1" -hyper = "0.14.27" +hyper = "0.14.27" hyper-tls = "0.5" http = "0.2.9" regex = "1.6.0" diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 7e9f1e98ca..86ea23b55b 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -144,15 +144,18 @@ impl Cmd { }), }; - let (result, meta, events) = client + let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; - tracing::trace!(?result); - tracing::trace!(?meta); + let events = res.events()?; if !events.is_empty() { tracing::info!("Events:\n {events:#?}"); } + let meta = res + .result_meta + .as_ref() + .ok_or(Error::MissingOperationResult)?; // The transaction from core will succeed regardless of whether it actually found & extended // the entry, so we have to inspect the result meta to tell if it worked or not. diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 905775298a..375bfd7aaf 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -112,14 +112,10 @@ impl Cmd { build_install_contract_code_tx(contract, sequence + 1, self.fee.fee, &key)?; // Currently internal errors are not returned if the contract code is expired - if let ( - TransactionResult { - result: TransactionResultResult::TxInternalError, - .. - }, - _, - _, - ) = client + if let Some(TransactionResult { + result: TransactionResultResult::TxInternalError, + .. + }) = client .prepare_and_send_transaction( &tx_without_preflight, &key, @@ -129,6 +125,8 @@ impl Cmd { None, ) .await? + .result + .as_ref() { // Now just need to restore it and don't have to install again restore::Cmd { diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 669342b065..eadc566f53 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -49,6 +49,9 @@ pub struct Cmd { /// Output the cost execution to stderr #[arg(long = "cost")] pub cost: bool, + /// Number of instructions to simulate + #[arg(long)] + pub instructions: Option, /// Function name as subcommand, then arguments for that function as `--arg-name value` #[arg(last = true, id = "CONTRACT_FN_AND_ARGS")] pub slop: Vec, @@ -300,28 +303,31 @@ impl Cmd { &key, )?; - let (result, meta, events) = client - .prepare_and_send_transaction( - &tx, - &key, - &signers, - &network.network_passphrase, - Some(log_events), - (global_args.verbose || global_args.very_verbose || self.cost) - .then_some(log_resources), + let mut txn = client.create_assembled_transaction(&tx).await?; + let (return_value, events) = if txn.is_view() { + ( + txn.sim_res().results()?[0].xdr.clone(), + txn.sim_res().events()?, ) - .await?; - - tracing::debug!(?result); - crate::log::diagnostic_events(&events, tracing::Level::INFO); - let xdr::TransactionMeta::V3(xdr::TransactionMetaV3 { - soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }), - .. - }) = meta - else { - return Err(Error::MissingOperationResult); + } else { + if let Some(instructions) = self.instructions { + txn = txn.set_max_instructions(instructions); + } + let res = client + .send_assembled_transaction( + txn, + &key, + &signers, + &network.network_passphrase, + Some(log_events), + (global_args.verbose || global_args.very_verbose || self.cost) + .then_some(log_resources), + ) + .await?; + (res.return_value()?, res.contract_events()?) }; + crate::log::diagnostic_events(&events, tracing::Level::INFO); output_to_string(&spec, &return_value, &function) } diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index 842832d5f3..f25b6c2c0e 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -120,7 +120,7 @@ impl Cmd { let ( LedgerKey::ContractData(LedgerKeyContractData { key, .. }), LedgerEntryData::ContractData(ContractDataEntry { val, .. }), - ) = (key, val) + ) = &(key, val) else { return Err(Error::OnlyDataAllowed); }; diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 38b8a84a19..6ed39f8929 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -148,11 +148,15 @@ impl Cmd { }), }; - let (result, meta, events) = client + let res = client .prepare_and_send_transaction(&tx, &key, &[], &network.network_passphrase, None, None) .await?; - tracing::trace!(?result); + let meta = res + .result_meta + .as_ref() + .ok_or(Error::MissingOperationResult)?; + let events = res.events()?; tracing::trace!(?meta); if !events.is_empty() { tracing::info!("Events:\n {events:#?}"); @@ -177,7 +181,7 @@ impl Cmd { operations[0].changes.len() ); } - parse_operations(&operations).ok_or(Error::MissingOperationResult) + parse_operations(operations).ok_or(Error::MissingOperationResult) } } diff --git a/cmd/soroban-cli/src/rpc/log.rs b/cmd/soroban-cli/src/rpc/log.rs new file mode 100644 index 0000000000..3612681484 --- /dev/null +++ b/cmd/soroban-cli/src/rpc/log.rs @@ -0,0 +1,2 @@ +pub mod diagnostic_events; +pub use diagnostic_events::*; diff --git a/cmd/soroban-cli/src/rpc/log/diagnostic_events.rs b/cmd/soroban-cli/src/rpc/log/diagnostic_events.rs new file mode 100644 index 0000000000..68af67a4eb --- /dev/null +++ b/cmd/soroban-cli/src/rpc/log/diagnostic_events.rs @@ -0,0 +1,11 @@ +pub fn diagnostic_events(events: &[impl std::fmt::Debug], level: tracing::Level) { + for (i, event) in events.iter().enumerate() { + if level == tracing::Level::TRACE { + tracing::trace!("{i}: {event:#?}"); + } else if level == tracing::Level::INFO { + tracing::info!("{i}: {event:#?}"); + } else if level == tracing::Level::ERROR { + tracing::error!("{i}: {event:#?}"); + } + } +} diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/soroban-cli/src/rpc/mod.rs index 535426292b..163d5b3463 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/soroban-cli/src/rpc/mod.rs @@ -21,14 +21,19 @@ use std::{ str::FromStr, time::{Duration, Instant}, }; +use stellar_xdr::curr::ContractEventType; use termcolor::{Color, ColorChoice, StandardStream, WriteColor}; use termcolor_output::colored; use tokio::time::sleep; -use crate::utils::contract_spec; - +pub mod log; mod txn; +pub use txn::*; + +use crate::utils::contract_spec::ContractSpec as Contract; + +use crate::utils::contract_spec as contract; const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); pub type LogEvents = fn( @@ -88,7 +93,7 @@ pub enum Error { #[error("unexpected contract code data type: {0:?}")] UnexpectedContractCodeDataType(LedgerEntryData), #[error(transparent)] - CouldNotParseContractSpec(#[from] contract_spec::Error), + CouldNotParseContractSpec(#[from] contract::Error), #[error("unexpected contract code got token")] UnexpectedToken(ContractDataEntry), #[error(transparent)] @@ -99,6 +104,11 @@ pub enum Error { LargeFee(u64), #[error("Cannot authorize raw transactions")] CannotAuthorizeRawTransaction, + + #[error("Missing result for tnx")] + MissingOp, + #[error("A simulation is not a transaction")] + NotSignedTransaction, } #[derive(serde::Deserialize, serde::Serialize, Debug)] @@ -170,6 +180,36 @@ impl TryInto for GetTransactionResponseRaw { } } +impl GetTransactionResponse { + pub fn return_value(&self) -> Result { + if let Some(xdr::TransactionMeta::V3(xdr::TransactionMetaV3 { + soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }), + .. + })) = self.result_meta.as_ref() + { + Ok(return_value.clone()) + } else { + Err(Error::MissingOp) + } + } + + pub fn events(&self) -> Result, Error> { + if let Some(meta) = self.result_meta.as_ref() { + Ok(extract_events(meta)) + } else { + Err(Error::MissingOp) + } + } + + pub fn contract_events(&self) -> Result, Error> { + Ok(self + .events()? + .into_iter() + .filter(|e| matches!(e.event.type_, ContractEventType::Contract)) + .collect::>()) + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct LedgerEntryResult { pub key: String, @@ -614,7 +654,7 @@ soroban config identity fund {address} --helper-url "# pub async fn send_transaction( &self, tx: &TransactionEnvelope, - ) -> Result<(TransactionResult, TransactionMeta, Vec), Error> { + ) -> Result { let client = self.client()?; tracing::trace!("Sending:\n{tx:#?}"); let SendTransactionResponse { @@ -656,14 +696,8 @@ soroban config identity fund {address} --helper-url "# "SUCCESS" => { // TODO: the caller should probably be printing this tracing::trace!("{response:#?}"); - let GetTransactionResponse { - result, - result_meta, - .. - } = response; - let meta = result_meta.ok_or(Error::MissingResult)?; - let events = extract_events(&meta); - return Ok((result.ok_or(Error::MissingResult)?, meta, events)); + + return Ok(response); } "FAILED" => { tracing::error!("{response:#?}"); @@ -703,22 +737,21 @@ soroban config identity fund {address} --helper-url "# match response.error { None => Ok(response), Some(e) => { - crate::log::diagnostic_events(&response.events, tracing::Level::ERROR); + log::diagnostic_events(&response.events, tracing::Level::ERROR); Err(Error::TransactionSimulationFailed(e)) } } } - pub async fn prepare_and_send_transaction( + pub async fn send_assembled_transaction( &self, - tx_without_preflight: &Transaction, + txn: txn::Assembled, source_key: &ed25519_dalek::SigningKey, signers: &[ed25519_dalek::SigningKey], network_passphrase: &str, log_events: Option, log_resources: Option, - ) -> Result<(TransactionResult, TransactionMeta, Vec), Error> { - let txn = txn::Assembled::new(tx_without_preflight, self).await?; + ) -> Result { let seq_num = txn.sim_res().latest_ledger + 60; //5 min; let authorized = txn .handle_restore(self, source_key, network_passphrase) @@ -726,10 +759,39 @@ soroban config identity fund {address} --helper-url "# .authorize(self, source_key, signers, seq_num, network_passphrase) .await?; authorized.log(log_events, log_resources)?; + let tx = authorized.sign(source_key, network_passphrase)?; self.send_transaction(&tx).await } + pub async fn prepare_and_send_transaction( + &self, + tx_without_preflight: &Transaction, + source_key: &ed25519_dalek::SigningKey, + signers: &[ed25519_dalek::SigningKey], + network_passphrase: &str, + log_events: Option, + log_resources: Option, + ) -> Result { + let txn = txn::Assembled::new(tx_without_preflight, self).await?; + self.send_assembled_transaction( + txn, + source_key, + signers, + network_passphrase, + log_events, + log_resources, + ) + .await + } + + pub async fn create_assembled_transaction( + &self, + txn: &Transaction, + ) -> Result { + txn::Assembled::new(txn, self).await + } + pub async fn get_transaction(&self, tx_id: &str) -> Result { Ok(self .client()? @@ -898,11 +960,9 @@ soroban config identity fund {address} --helper-url "# xdr::ScVal::ContractInstance(xdr::ScContractInstance { executable: xdr::ContractExecutable::Wasm(hash), .. - }) => Ok(contract_spec::ContractSpec::new( - &self.get_remote_wasm_from_hash(hash).await?, - ) - .map_err(Error::CouldNotParseContractSpec)? - .spec), + }) => Ok(Contract::new(&self.get_remote_wasm_from_hash(hash).await?) + .map_err(Error::CouldNotParseContractSpec)? + .spec), xdr::ScVal::ContractInstance(xdr::ScContractInstance { executable: xdr::ContractExecutable::StellarAsset, .. diff --git a/cmd/soroban-cli/src/rpc/txn.rs b/cmd/soroban-cli/src/rpc/txn.rs index 9e36938ddd..ece4f7fe45 100644 --- a/cmd/soroban-cli/src/rpc/txn.rs +++ b/cmd/soroban-cli/src/rpc/txn.rs @@ -2,16 +2,16 @@ use ed25519_dalek::Signer; use sha2::{Digest, Sha256}; use soroban_env_host::xdr::{ self, AccountId, DecoratedSignature, ExtensionPoint, Hash, HashIdPreimage, - HashIdPreimageSorobanAuthorization, InvokeHostFunctionOp, Limits, Memo, Operation, - OperationBody, Preconditions, PublicKey, ReadXdr, RestoreFootprintOp, ScAddress, ScMap, - ScSymbol, ScVal, Signature, SignatureHint, SorobanAddressCredentials, + HashIdPreimageSorobanAuthorization, InvokeHostFunctionOp, LedgerFootprint, Limits, Memo, + Operation, OperationBody, Preconditions, PublicKey, ReadXdr, RestoreFootprintOp, ScAddress, + ScMap, ScSymbol, ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanCredentials, SorobanResources, SorobanTransactionData, Transaction, TransactionEnvelope, TransactionExt, TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, Uint256, VecM, WriteXdr, }; -use crate::rpc::{Client, Error, RestorePreamble, SimulateTransactionResponse}; +use super::{Client, Error, RestorePreamble, SimulateTransactionResponse}; use super::{LogEvents, LogResources}; @@ -117,6 +117,7 @@ impl Assembled { } } + #[must_use] pub fn bump_seq_num(mut self) -> Self { self.txn.seq_num.0 += 1; self @@ -156,6 +157,44 @@ impl Assembled { } Ok(()) } + + pub fn requires_auth(&self) -> bool { + requires_auth(&self.txn) + } + + pub fn is_view(&self) -> bool { + if let TransactionExt::V1(SorobanTransactionData { + resources: + SorobanResources { + footprint: LedgerFootprint { read_write, .. }, + .. + }, + .. + }) = &self.txn.ext + { + if read_write.is_empty() { + return true; + } + }; + !self.requires_auth() + } + + #[must_use] + pub fn set_max_instructions(mut self, instructions: u32) -> Self { + if let TransactionExt::V1(SorobanTransactionData { + resources: + SorobanResources { + instructions: ref mut i, + .. + }, + .. + }) = &mut self.txn.ext + { + tracing::trace!("setting max instructions to {instructions} from {i}"); + *i = instructions; + } + self + } } // Apply the result of a simulateTransaction onto a transaction envelope, preparing it for @@ -206,7 +245,7 @@ pub fn assemble( } // update the fees of the actual transaction to meet the minimum resource fees. - let classic_transaction_fees = crate::fee::Args::default().fee; + let classic_transaction_fees = 100; // Pad the fees up by 15% for a bit of wiggle room. tx.fee = (tx.fee.max( classic_transaction_fees @@ -220,6 +259,20 @@ pub fn assemble( Ok(tx) } +fn requires_auth(txn: &Transaction) -> bool { + let [Operation { + body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), + .. + }] = txn.operations.as_slice() + else { + return false; + }; + matches!( + auth.first().map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + ) +} + // Use the given source_key and signers, to sign all SorobanAuthorizationEntry's in the given // transaction. If unable to sign, return an error. fn sign_soroban_authorizations( @@ -230,18 +283,10 @@ fn sign_soroban_authorizations( network_passphrase: &str, ) -> Result, Error> { let mut tx = raw.clone(); - let mut op = match tx.operations.as_slice() { - [op @ Operation { - body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), - .. - }] if matches!( - auth.first().map(|x| &x.root_invocation.function), - Some(&SorobanAuthorizedFunction::ContractFn(_)) - ) => - { - op.clone() - } - _ => return Ok(None), + let mut op = if requires_auth(&tx) { + tx.operations[0].clone() + } else { + return Ok(None); }; let Operation { diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go index 997372ed89..517c03962e 100644 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ b/cmd/soroban-rpc/internal/test/cli_test.go @@ -36,7 +36,7 @@ func cargoTest(t *testing.T, name string) { } func TestCLICargoTest(t *testing.T) { - names := icmd.RunCmd(icmd.Command("cargo", "-q", "test", "integration::", "--package", "soroban-test", "--features", "integration", "--", "--list")) + names := icmd.RunCmd(icmd.Command("cargo", "-q", "test", "integration::hello_world::contract_data_read", "--package", "soroban-test", "--features", "integration", "--", "--list")) input := names.Stdout() lines := strings.Split(strings.TrimSpace(input), "\n") for _, line := range lines { diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index 3546594c61..1e045fc537 100644 --- a/docs/soroban-cli-full-docs.md +++ b/docs/soroban-cli-full-docs.md @@ -735,6 +735,7 @@ soroban contract invoke ... -- --help * `--id ` — Contract ID to invoke * `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server * `--network ` — Name of network to use from config