From ed308cc0851553bf00ec5721c253c8da84419cc5 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 6 Feb 2024 12:56:36 -0500 Subject: [PATCH] [Epic] Separating soroban-rpc to prepare for repo change (#1174) * feat: move ContractSpec to spec tools crate * feat: add methods to GetTransaction and break up sending transaction * feat: make persistent the default storage type * feat: create soroban-rpc crate (#21) * feat: create soroban-rpc crate * fix: add event test * fix: switch to --is-view so e2e tests will pass and no breaking changes --- Cargo.toml | 28 ++- cmd/crates/soroban-rpc/Cargo.toml | 51 +++++ cmd/crates/soroban-rpc/README.md | 3 + .../src}/fixtures/event_response.json | 0 .../mod.rs => crates/soroban-rpc/src/lib.rs} | 170 ++++++++++++--- cmd/crates/soroban-rpc/src/log.rs | 2 + .../soroban-rpc/src/log/diagnostic_events.rs | 11 + .../src/rpc => crates/soroban-rpc/src}/txn.rs | 166 +++++++++++--- cmd/crates/soroban-spec-tools/Cargo.toml | 2 + .../soroban-spec-tools/src/contract.rs} | 8 +- cmd/crates/soroban-spec-tools/src/lib.rs | 1 + cmd/crates/soroban-test/tests/it/help.rs | 4 +- .../tests/it/integration/dotenv.rs | 7 +- .../tests/it/integration/hello_world.rs | 18 ++ cmd/crates/soroban-test/tests/it/util.rs | 2 + cmd/soroban-cli/Cargo.toml | 15 +- .../commands/contract/bindings/typescript.rs | 16 +- .../src/commands/contract/extend.rs | 9 +- .../src/commands/contract/inspect.rs | 3 +- .../src/commands/contract/install.rs | 16 +- .../src/commands/contract/invoke.rs | 63 +++--- cmd/soroban-cli/src/commands/contract/read.rs | 2 +- .../src/commands/contract/restore.rs | 10 +- cmd/soroban-cli/src/key.rs | 4 +- cmd/soroban-cli/src/lib.rs | 6 +- cmd/soroban-cli/src/utils.rs | 57 +---- cmd/soroban-cli/src/wasm.rs | 9 +- docs/soroban-cli-full-docs.md | 203 ++++++++++++++++++ 28 files changed, 700 insertions(+), 186 deletions(-) create mode 100644 cmd/crates/soroban-rpc/Cargo.toml create mode 100644 cmd/crates/soroban-rpc/README.md rename cmd/{soroban-cli/src/rpc => crates/soroban-rpc/src}/fixtures/event_response.json (100%) rename cmd/{soroban-cli/src/rpc/mod.rs => crates/soroban-rpc/src/lib.rs} (92%) create mode 100644 cmd/crates/soroban-rpc/src/log.rs create mode 100644 cmd/crates/soroban-rpc/src/log/diagnostic_events.rs rename cmd/{soroban-cli/src/rpc => crates/soroban-rpc/src}/txn.rs (82%) rename cmd/{soroban-cli/src/utils/contract_spec.rs => crates/soroban-spec-tools/src/contract.rs} (98%) diff --git a/Cargo.toml b/Cargo.toml index 8758b42ce3..3e55a7e8df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,27 +69,49 @@ version = "=20.3.1" version = "20.3.0" path = "cmd/soroban-cli" +[workspace.dependencies.soroban-rpc] +version = "20.2.0" +path = "cmd/crates/soroban-rpc" + [workspace.dependencies.stellar-xdr] version = "=20.1.0" 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-rpc/Cargo.toml b/cmd/crates/soroban-rpc/Cargo.toml new file mode 100644 index 0000000000..966597a22b --- /dev/null +++ b/cmd/crates/soroban-rpc/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "soroban-rpc" +description = "Soroban RPC client for rust" +homepage = "https://github.com/stellar/soroban-tools" +repository = "https://github.com/stellar/soroban-tools" +authors = ["Stellar Development Foundation "] +license = "Apache-2.0" +readme = "README.md" +version.workspace = true +edition = "2021" +rust-version.workspace = true +autobins = false + + +[lib] +crate-type = ["rlib"] + + +[dependencies] +soroban-sdk = { version = "=20.3.0", git = "https://github.com/stellar/rs-soroban-sdk", rev = "4aef54ff9295c2fca4c5b9fbd2c92d0ff99f67de" } +soroban-spec-tools = { version = "20.2.0", path = "../soroban-spec-tools" } +soroban-env-host = { version = "=20.2.0", git = "https://github.com/stellar/rs-soroban-env", rev = "1bfc0f2a2ee134efc1e1b0d5270281d0cba61c2e" } +stellar-strkey = "0.0.7" +stellar-xdr = { version = "=20.1.0", default-features = true, features = ["curr", "std", "serde"] } +soroban-spec = { version = "=20.3.0", git = "https://github.com/stellar/rs-soroban-sdk", rev = "4aef54ff9295c2fca4c5b9fbd2c92d0ff99f67de" } + +termcolor = "1.1.3" +termcolor_output = "1.0.1" +clap = { version = "4.1.8", features = ["derive", "env", "deprecated", "string"] } +serde_json = "1.0.82" +serde-aux = "4.1.2" +itertools = "0.10.0" +ethnum = "1.3.2" +hex = "0.4.3" +wasmparser = "0.90.0" +base64 = "0.21.2" +thiserror = "1.0.46" +serde = "1.0.82" +tokio = "1.28.1" +sha2 = "0.10.7" +ed25519-dalek = "2.0.0" +tracing = "0.1.40" + +# networking +jsonrpsee-http-client = "0.20.1" +jsonrpsee-core = "0.20.1" +http = "1.0.0" + + +[dev-dependencies] +which = "4.4.0" diff --git a/cmd/crates/soroban-rpc/README.md b/cmd/crates/soroban-rpc/README.md new file mode 100644 index 0000000000..9185b7fd05 --- /dev/null +++ b/cmd/crates/soroban-rpc/README.md @@ -0,0 +1,3 @@ +# soroban-rpc + +Tools and utilities for soroban rpc. diff --git a/cmd/soroban-cli/src/rpc/fixtures/event_response.json b/cmd/crates/soroban-rpc/src/fixtures/event_response.json similarity index 100% rename from cmd/soroban-cli/src/rpc/fixtures/event_response.json rename to cmd/crates/soroban-rpc/src/fixtures/event_response.json diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/crates/soroban-rpc/src/lib.rs similarity index 92% rename from cmd/soroban-cli/src/rpc/mod.rs rename to cmd/crates/soroban-rpc/src/lib.rs index 535426292b..95c3692e1f 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/crates/soroban-rpc/src/lib.rs @@ -21,15 +21,20 @@ 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::Assembled; + +use soroban_spec_tools::contract; + const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); +pub(crate) const DEFAULT_TRANSACTION_FEES: u32 = 100; pub type LogEvents = fn( footprint: &LedgerFootprint, @@ -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,9 @@ pub enum Error { LargeFee(u64), #[error("Cannot authorize raw transactions")] CannotAuthorizeRawTransaction, + + #[error("Missing result for tnx")] + MissingOp, } #[derive(serde::Deserialize, serde::Serialize, Debug)] @@ -170,6 +178,41 @@ impl TryInto for GetTransactionResponseRaw { } } +impl GetTransactionResponse { + /// + /// # Errors + pub fn return_value(&self) -> Result { + if let Some(xdr::TransactionMeta::V3(xdr::TransactionMetaV3 { + soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }), + .. + })) = &self.result_meta + { + Ok(return_value.clone()) + } else { + Err(Error::MissingOp) + } + } + + /// + /// # Errors + pub fn events(&self) -> Result, Error> { + self.result_meta + .as_ref() + .map(extract_events) + .ok_or(Error::MissingOp) + } + + /// + /// # Errors + 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, @@ -273,6 +316,8 @@ pub struct SimulateTransactionResponse { } impl SimulateTransactionResponse { + /// + /// # Errors pub fn results(&self) -> Result, Error> { self.results .iter() @@ -294,6 +339,8 @@ impl SimulateTransactionResponse { .collect() } + /// + /// # Errors pub fn events(&self) -> Result, Error> { self.events .iter() @@ -301,6 +348,8 @@ impl SimulateTransactionResponse { .collect() } + /// + /// # Errors pub fn transaction_data(&self) -> Result { Ok(SorobanTransactionData::from_xdr_base64( &self.transaction_data, @@ -343,6 +392,7 @@ pub struct GetEventsResponse { // Reference](https://docs.google.com/document/d/1TZUDgo_3zPz7TiPMMHVW_mtogjLyPL0plvzGMsxSz6A/edit#bookmark=id.35t97rnag3tx) // [Code // Reference](https://github.com/stellar/soroban-tools/blob/bac1be79e8c2590c9c35ad8a0168aab0ae2b4171/cmd/soroban-rpc/internal/methods/get_events.go#L182-L203) +#[must_use] pub fn does_topic_match(topic: &[String], filter: &[String]) -> bool { filter.len() == topic.len() && filter @@ -397,10 +447,13 @@ impl Display for Event { } impl Event { + /// + /// # Errors pub fn parse_cursor(&self) -> Result<(u64, i32), Error> { parse_cursor(&self.id) } - + /// + /// # Errors pub fn pretty_print(&self) -> Result<(), Box> { let mut stdout = StandardStream::stdout(ColorChoice::Auto); if !stdout.supports_color() { @@ -502,6 +555,8 @@ pub struct Client { } impl Client { + /// + /// # Errors pub fn new(base_url: &str) -> Result { // Add the port to the base URL if there is no port explicitly included // in the URL and the scheme allows us to infer a default port. @@ -532,6 +587,8 @@ impl Client { }) } + /// + /// # Errors fn client(&self) -> Result { let url = self.base_url.clone(); let mut headers = HeaderMap::new(); @@ -543,6 +600,8 @@ impl Client { .build(url)?) } + /// + /// # Errors pub async fn friendbot_url(&self) -> Result { let network = self.get_network().await?; tracing::trace!("{network:#?}"); @@ -553,7 +612,8 @@ impl Client { ) }) } - + /// + /// # Errors pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result { let server = self.get_network().await?.passphrase; if let Some(expected) = expected { @@ -567,11 +627,15 @@ impl Client { Ok(server) } + /// + /// # Errors pub async fn get_network(&self) -> Result { tracing::trace!("Getting network"); Ok(self.client()?.request("getNetwork", rpc_params![]).await?) } + /// + /// # Errors pub async fn get_latest_ledger(&self) -> Result { tracing::trace!("Getting latest ledger"); Ok(self @@ -580,6 +644,8 @@ impl Client { .await?) } + /// + /// # Errors pub async fn get_account(&self, address: &str) -> Result { tracing::trace!("Getting address {}", address); let key = LedgerKey::Account(LedgerKeyAccount { @@ -611,10 +677,12 @@ soroban config identity fund {address} --helper-url "# } } + /// + /// # Errors 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 +724,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:#?}"); @@ -687,6 +749,8 @@ soroban config identity fund {address} --helper-url "# } } + /// + /// # Errors pub async fn simulate_transaction( &self, tx: &TransactionEnvelope, @@ -703,33 +767,69 @@ 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( + /// + /// # Errors + 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?; - let seq_num = txn.sim_res().latest_ledger + 60; //5 min; + ) -> Result { + let seq_num = txn.sim_response().latest_ledger + 60; //5 min; let authorized = txn .handle_restore(self, source_key, network_passphrase) .await? .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 } + /// + /// # Errors + 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 + } + + /// + /// # Errors + pub async fn create_assembled_transaction( + &self, + txn: &Transaction, + ) -> Result { + txn::Assembled::new(txn, self).await + } + + /// + /// # Errors pub async fn get_transaction(&self, tx_id: &str) -> Result { Ok(self .client()? @@ -737,6 +837,8 @@ soroban config identity fund {address} --helper-url "# .await?) } + /// + /// # Errors pub async fn get_ledger_entries( &self, keys: &[LedgerKey], @@ -747,7 +849,7 @@ soroban config identity fund {address} --helper-url "# if base64_result.is_err() { return Err(Error::Xdr(XdrError::Invalid)); } - base64_keys.push(k.to_xdr_base64(Limits::none()).unwrap()); + base64_keys.push(k.to_xdr_base64(Limits::none())?); } Ok(self .client()? @@ -755,6 +857,8 @@ soroban config identity fund {address} --helper-url "# .await?) } + /// + /// # Errors pub async fn get_full_ledger_entries( &self, ledger_keys: &[LedgerKey], @@ -795,7 +899,8 @@ soroban config identity fund {address} --helper-url "# latest_ledger, }) } - + /// + /// # Errors pub async fn get_events( &self, start: EventStart, @@ -835,6 +940,8 @@ soroban config identity fund {address} --helper-url "# Ok(self.client()?.request("getEvents", oparams).await?) } + /// + /// # Errors pub async fn get_contract_data( &self, contract_id: &[u8; 32], @@ -858,6 +965,8 @@ soroban config identity fund {address} --helper-url "# } } + /// + /// # Errors pub async fn get_remote_wasm(&self, contract_id: &[u8; 32]) -> Result, Error> { match self.get_contract_data(contract_id).await? { xdr::ContractDataEntry { @@ -872,6 +981,8 @@ soroban config identity fund {address} --helper-url "# } } + /// + /// # Errors pub async fn get_remote_wasm_from_hash(&self, hash: xdr::Hash) -> Result, Error> { let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() }); let contract_data = self.get_ledger_entries(&[code_key]).await?; @@ -888,7 +999,8 @@ soroban config identity fund {address} --helper-url "# scval => Err(Error::UnexpectedContractCodeDataType(scval)), } } - + /// + /// # Errors pub async fn get_remote_contract_spec( &self, contract_id: &[u8; 32], @@ -898,11 +1010,11 @@ 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::Spec::new(&self.get_remote_wasm_from_hash(hash).await?) + .map_err(Error::CouldNotParseContractSpec)? + .spec, + ), xdr::ScVal::ContractInstance(xdr::ScContractInstance { executable: xdr::ContractExecutable::StellarAsset, .. @@ -939,7 +1051,7 @@ fn extract_events(tx_meta: &TransactionMeta) -> Vec { } } -pub fn parse_cursor(c: &str) -> Result<(u64, i32), Error> { +pub(crate) fn parse_cursor(c: &str) -> Result<(u64, i32), Error> { let (toid_part, event_index) = c.split('-').collect_tuple().ok_or(Error::InvalidCursor)?; let toid_part: u64 = toid_part.parse().map_err(|_| Error::InvalidCursor)?; let start_index: i32 = event_index.parse().map_err(|_| Error::InvalidCursor)?; diff --git a/cmd/crates/soroban-rpc/src/log.rs b/cmd/crates/soroban-rpc/src/log.rs new file mode 100644 index 0000000000..3612681484 --- /dev/null +++ b/cmd/crates/soroban-rpc/src/log.rs @@ -0,0 +1,2 @@ +pub mod diagnostic_events; +pub use diagnostic_events::*; diff --git a/cmd/crates/soroban-rpc/src/log/diagnostic_events.rs b/cmd/crates/soroban-rpc/src/log/diagnostic_events.rs new file mode 100644 index 0000000000..68af67a4eb --- /dev/null +++ b/cmd/crates/soroban-rpc/src/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/txn.rs b/cmd/crates/soroban-rpc/src/txn.rs similarity index 82% rename from cmd/soroban-cli/src/rpc/txn.rs rename to cmd/crates/soroban-rpc/src/txn.rs index 9e36938ddd..35b0c71924 100644 --- a/cmd/soroban-cli/src/rpc/txn.rs +++ b/cmd/crates/soroban-rpc/src/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}; @@ -20,13 +20,35 @@ pub struct Assembled { sim_res: SimulateTransactionResponse, } +/// Represents an assembled transaction ready to be signed and submitted to the network. impl Assembled { + /// + /// Creates a new `Assembled` transaction. + /// + /// # Arguments + /// + /// * `txn` - The original transaction. + /// * `client` - The client used for simulation and submission. + /// + /// # Errors + /// + /// Returns an error if simulation fails or if assembling the transaction fails. pub async fn new(txn: &Transaction, client: &Client) -> Result { let sim_res = Self::simulate(txn, client).await?; let txn = assemble(txn, &sim_res)?; Ok(Self { txn, sim_res }) } + /// + /// Calculates the hash of the assembled transaction. + /// + /// # Arguments + /// + /// * `network_passphrase` - The network passphrase. + /// + /// # Errors + /// + /// Returns an error if generating the hash fails. pub fn hash(&self, network_passphrase: &str) -> Result<[u8; 32], xdr::Error> { let signature_payload = TransactionSignaturePayload { network_id: Hash(Sha256::digest(network_passphrase).into()), @@ -35,12 +57,23 @@ impl Assembled { Ok(Sha256::digest(signature_payload.to_xdr(Limits::none())?).into()) } + /// + /// Signs the assembled transaction. + /// + /// # Arguments + /// + /// * `key` - The signing key. + /// * `network_passphrase` - The network passphrase. + /// + /// # Errors + /// + /// Returns an error if signing the transaction fails. pub fn sign( self, key: &ed25519_dalek::SigningKey, network_passphrase: &str, ) -> Result { - let tx = self.txn(); + let tx = self.transaction(); let tx_hash = self.hash(network_passphrase)?; let tx_signature = key.sign(&tx_hash); @@ -55,6 +88,17 @@ impl Assembled { })) } + /// + /// Simulates the assembled transaction. + /// + /// # Arguments + /// + /// * `tx` - The original transaction. + /// * `client` - The client used for simulation. + /// + /// # Errors + /// + /// Returns an error if simulation fails. pub async fn simulate( tx: &Transaction, client: &Client, @@ -67,6 +111,18 @@ impl Assembled { .await } + /// + /// Handles the restore process for the assembled transaction. + /// + /// # Arguments + /// + /// * `client` - The client used for submission. + /// * `source_key` - The signing key of the source account. + /// * `network_passphrase` - The network passphrase. + /// + /// # Errors + /// + /// Returns an error if the restore process fails. pub async fn handle_restore( self, client: &Client, @@ -77,7 +133,7 @@ impl Assembled { // Build and submit the restore transaction client .send_transaction( - &Assembled::new(&restore(self.txn(), restore_preamble)?, client) + &Assembled::new(&restore(self.transaction(), restore_preamble)?, client) .await? .sign(source_key, network_passphrase)?, ) @@ -88,14 +144,20 @@ impl Assembled { } } - pub fn txn(&self) -> &Transaction { + /// Returns a reference to the original transaction. + #[must_use] + pub fn transaction(&self) -> &Transaction { &self.txn } - pub fn sim_res(&self) -> &SimulateTransactionResponse { + /// Returns a reference to the simulation response. + #[must_use] + pub fn sim_response(&self) -> &SimulateTransactionResponse { &self.sim_res } + /// + /// # Errors pub async fn authorize( self, client: &Client, @@ -105,7 +167,7 @@ impl Assembled { network_passphrase: &str, ) -> Result { if let Some(txn) = sign_soroban_authorizations( - self.txn(), + self.transaction(), source_key, signers, seq_num, @@ -117,12 +179,16 @@ impl Assembled { } } + #[must_use] pub fn bump_seq_num(mut self) -> Self { self.txn.seq_num.0 += 1; self } - pub fn auth(&self) -> VecM { + /// + /// # Errors + #[must_use] + pub fn auth_entries(&self) -> VecM { self.txn .operations .first() @@ -137,6 +203,8 @@ impl Assembled { .unwrap_or_default() } + /// + /// # Errors pub fn log( &self, log_events: Option, @@ -151,15 +219,55 @@ impl Assembled { log(resources); } if let Some(log) = log_events { - log(footprint, &[self.auth()], &self.sim_res.events()?); + log(footprint, &[self.auth_entries()], &self.sim_res.events()?); }; } Ok(()) } + + #[must_use] + pub fn requires_auth(&self) -> bool { + requires_auth(&self.txn).is_some() + } + + #[must_use] + pub fn is_view(&self) -> bool { + let TransactionExt::V1(SorobanTransactionData { + resources: + SorobanResources { + footprint: LedgerFootprint { read_write, .. }, + .. + }, + .. + }) = &self.txn.ext + else { + return false; + }; + read_write.is_empty() + } + + #[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 // submission to the network. +/// +/// # Errors pub fn assemble( raw: &Transaction, simulation: &SimulateTransactionResponse, @@ -206,7 +314,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 = crate::DEFAULT_TRANSACTION_FEES; // Pad the fees up by 15% for a bit of wiggle room. tx.fee = (tx.fee.max( classic_transaction_fees @@ -220,6 +328,21 @@ pub fn assemble( Ok(tx) } +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()) +} + // 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 +353,8 @@ 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 Some(mut op) = requires_auth(&tx) else { + return Ok(None); }; let Operation { @@ -378,6 +491,8 @@ fn sign_soroban_authorization_entry( Ok(auth) } +/// +/// # Errors pub fn restore(parent: &Transaction, restore: &RestorePreamble) -> Result { let transaction_data = SorobanTransactionData::from_xdr_base64(&restore.transaction_data, Limits::none())?; @@ -398,8 +513,7 @@ pub fn restore(parent: &Transaction, restore: &RestorePreamble) -> Result, pub env_meta: Vec, pub meta_base64: Option, @@ -38,7 +38,7 @@ pub enum Error { Parser(#[from] wasmparser::BinaryReaderError), } -impl ContractSpec { +impl Spec { pub fn new(bytes: &[u8]) -> Result { let mut env_meta: Option<&[u8]> = None; let mut meta: Option<&[u8]> = None; @@ -87,7 +87,7 @@ impl ContractSpec { vec![] }; - Ok(ContractSpec { + Ok(Spec { env_meta_base64, env_meta, meta_base64, @@ -108,7 +108,7 @@ impl ContractSpec { } } -impl Display for ContractSpec { +impl Display for Spec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(env_meta) = &self.env_meta_base64 { writeln!(f, "Env Meta: {env_meta}")?; diff --git a/cmd/crates/soroban-spec-tools/src/lib.rs b/cmd/crates/soroban-spec-tools/src/lib.rs index 7f310fd996..bc3c2a3866 100644 --- a/cmd/crates/soroban-spec-tools/src/lib.rs +++ b/cmd/crates/soroban-spec-tools/src/lib.rs @@ -13,6 +13,7 @@ use stellar_xdr::curr::{ UInt128Parts, UInt256Parts, Uint256, VecM, }; +pub mod contract; pub mod utils; #[derive(thiserror::Error, Debug)] diff --git a/cmd/crates/soroban-test/tests/it/help.rs b/cmd/crates/soroban-test/tests/it/help.rs index 6d4680e772..a66c449ed3 100644 --- a/cmd/crates/soroban-test/tests/it/help.rs +++ b/cmd/crates/soroban-test/tests/it/help.rs @@ -1,11 +1,11 @@ use soroban_cli::commands::contract; use soroban_test::TestEnv; -use crate::util::{invoke_custom as invoke, CUSTOM_TYPES}; +use crate::util::{invoke_custom as invoke, CUSTOM_TYPES, DEFAULT_CONTRACT_ID}; async fn invoke_custom(func: &str, args: &str) -> Result { let e = &TestEnv::default(); - invoke(e, "1", func, args, &CUSTOM_TYPES.path()).await + invoke(e, DEFAULT_CONTRACT_ID, func, args, &CUSTOM_TYPES.path()).await } #[tokio::test] diff --git a/cmd/crates/soroban-test/tests/it/integration/dotenv.rs b/cmd/crates/soroban-test/tests/it/integration/dotenv.rs index d7d56aaf3b..7c0f25b3fc 100644 --- a/cmd/crates/soroban-test/tests/it/integration/dotenv.rs +++ b/cmd/crates/soroban-test/tests/it/integration/dotenv.rs @@ -35,7 +35,7 @@ fn current_env_not_overwritten() { write_env_file(e, &contract_id()); e.new_assert_cmd("contract") - .env("SOROBAN_CONTRACT_ID", "2") + .env("SOROBAN_CONTRACT_ID", "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4") .arg("invoke") .arg("--") .arg("hello") @@ -51,7 +51,10 @@ fn cli_args_have_priority() { deploy_hello(e); write_env_file(e, &contract_id()); e.new_assert_cmd("contract") - .env("SOROBAN_CONTRACT_ID", "2") + .env( + "SOROBAN_CONTRACT_ID", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + ) .arg("invoke") .arg("--id") .arg(TEST_CONTRACT_ID) 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..7bd8d596a7 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -1,3 +1,4 @@ +use predicates::boolean::PredicateBooleanExt; use soroban_cli::commands::{ contract::{self, fetch}, keys, @@ -19,7 +20,23 @@ async fn invoke() { extend_contract(sandbox, id, HELLO_WORLD).await; // Note that all functions tested here have no state invoke_hello_world(sandbox, id); + sandbox + .new_assert_cmd("events") + .arg("--start-ledger=20") + .arg("--id") + .arg(id) + .assert() + .stdout(predicates::str::contains(id).not()) + .success(); invoke_hello_world_with_lib(sandbox, id).await; + sandbox + .new_assert_cmd("events") + .arg("--start-ledger=20") + .arg("--id") + .arg(id) + .assert() + .stdout(predicates::str::contains(id)) + .success(); invoke_hello_world_with_lib_two(sandbox, id).await; invoke_auth(sandbox, id); invoke_auth_with_identity(sandbox, id).await; @@ -39,6 +56,7 @@ fn invoke_hello_world(sandbox: &TestEnv, id: &str) { sandbox .new_assert_cmd("contract") .arg("invoke") + .arg("--is-view") .arg("--id") .arg(id) .arg("--") diff --git a/cmd/crates/soroban-test/tests/it/util.rs b/cmd/crates/soroban-test/tests/it/util.rs index 6d62510126..112d5f841a 100644 --- a/cmd/crates/soroban-test/tests/it/util.rs +++ b/cmd/crates/soroban-test/tests/it/util.rs @@ -68,3 +68,5 @@ pub async fn invoke_custom( i.invoke(&soroban_cli::commands::global::Args::default()) .await } + +pub const DEFAULT_CONTRACT_ID: &str = "CDR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OO5Z"; diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index bd68ae2bb8..00981a1b93 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -45,24 +45,27 @@ soroban-spec-typescript = { workspace = true } soroban-ledger-snapshot = { workspace = true } stellar-strkey = { workspace = true } soroban-sdk = { workspace = true } -clap = { version = "4.1.8", features = [ +soroban-rpc = { workspace = true } + +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 +73,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/bindings/typescript.rs b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs index 19c7eecd66..091f8255a9 100644 --- a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs +++ b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs @@ -1,17 +1,15 @@ use std::{ffi::OsString, fmt::Debug, path::PathBuf}; use clap::{command, Parser}; +use soroban_spec_tools::contract as contract_spec; use soroban_spec_typescript::{self as typescript, boilerplate::Project}; -use crate::wasm; -use crate::{ - commands::{ - config::locator, - contract::{self, fetch}, - network::{self, Network}, - }, - utils::contract_spec::{self, ContractSpec}, +use crate::commands::{ + config::locator, + contract::{self, fetch}, + network::{self, Network}, }; +use crate::wasm; #[derive(Parser, Debug, Clone)] #[group(skip)] @@ -78,7 +76,7 @@ impl Cmd { network: self.network.clone(), }; let bytes = fetch.get_bytes().await?; - ContractSpec::new(&bytes)?.spec + contract_spec::Spec::new(&bytes)?.spec }; if self.output_dir.is_file() { return Err(Error::IsFile(self.output_dir.clone())); 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/inspect.rs b/cmd/soroban-cli/src/commands/contract/inspect.rs index 355c18ca81..e66bf83fdb 100644 --- a/cmd/soroban-cli/src/commands/contract/inspect.rs +++ b/cmd/soroban-cli/src/commands/contract/inspect.rs @@ -1,5 +1,6 @@ use clap::{command, Parser}; use soroban_env_host::xdr; +use soroban_spec_tools::contract; use std::{fmt::Debug, path::PathBuf}; use tracing::debug; @@ -28,7 +29,7 @@ pub enum Error { #[error(transparent)] Xdr(#[from] xdr::Error), #[error(transparent)] - Spec(#[from] crate::utils::contract_spec::Error), + Spec(#[from] contract::Error), } impl Cmd { diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 905775298a..c2fa6d889d 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 { @@ -153,7 +151,7 @@ impl Cmd { } } -fn get_contract_meta_sdk_version(wasm_spec: &utils::contract_spec::ContractSpec) -> Option { +fn get_contract_meta_sdk_version(wasm_spec: &soroban_spec_tools::contract::Spec) -> Option { let rs_sdk_version_option = if let Some(_meta) = &wasm_spec.meta_base64 { wasm_spec.meta.iter().find(|entry| match entry { ScMetaEntry::ScMetaV0(ScMetaV0 { key, .. }) => { diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 669342b065..720c3c6f80 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -28,13 +28,8 @@ use super::super::{ config::{self, locator}, events, }; -use crate::{ - commands::global, - rpc::{self, Client}, - utils::{self, contract_spec}, - Pwd, -}; -use soroban_spec_tools::Spec; +use crate::{commands::global, rpc, Pwd}; +use soroban_spec_tools::{contract, Spec}; #[derive(Parser, Debug, Default, Clone)] #[allow(clippy::struct_excessive_bools)] @@ -49,6 +44,12 @@ 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, + /// Do not sign and submit transaction + #[arg(long, env = "SOROBAN_INVOKE_SIGN", env = "SYSTEM_TEST_VERBOSE_OUTPUT")] + pub is_view: bool, /// Function name as subcommand, then arguments for that function as `--arg-name value` #[arg(last = true, id = "CONTRACT_FN_AND_ARGS")] pub slop: Vec, @@ -140,7 +141,7 @@ pub enum Error { #[error(transparent)] StrKey(#[from] stellar_strkey::DecodeError), #[error(transparent)] - ContractSpec(#[from] contract_spec::Error), + ContractSpec(#[from] contract::Error), #[error("")] MissingFileArg(PathBuf), } @@ -275,7 +276,7 @@ impl Cmd { // For testing wasm arg parsing let _ = self.build_host_function_parameters(contract_id, spec_entries)?; } - let client = Client::new(&network.rpc_url)?; + let client = rpc::Client::new(&network.rpc_url)?; client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; @@ -299,29 +300,31 @@ impl Cmd { self.fee.fee, &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?; + if let Some(instructions) = self.instructions { + txn = txn.set_max_instructions(instructions); + } + let (return_value, events) = if self.is_view { + ( + txn.sim_response().results()?[0].xdr.clone(), + txn.sim_response().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 { + 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) } @@ -344,7 +347,7 @@ impl Cmd { impl Cmd { fn contract_id(&self) -> Result<[u8; 32], Error> { - utils::contract_id_from_str(&self.contract_id) + soroban_spec_tools::utils::contract_id_from_str(&self.contract_id) .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) } } 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/key.rs b/cmd/soroban-cli/src/key.rs index e9901abd2c..2290e857e4 100644 --- a/cmd/soroban-cli/src/key.rs +++ b/cmd/soroban-cli/src/key.rs @@ -59,7 +59,7 @@ pub struct Args { )] pub wasm_hash: Option, /// Storage entry durability - #[arg(long, value_enum, required = true)] + #[arg(long, value_enum, required = true, default_value = "persistent")] pub durability: Durability, } @@ -83,7 +83,7 @@ impl Args { } else if let Some(wasm_hash) = &self.wasm_hash { return Ok(vec![LedgerKey::ContractCode(LedgerKeyContractCode { hash: xdr::Hash( - utils::contract_id_from_str(wasm_hash) + soroban_spec_tools::utils::contract_id_from_str(wasm_hash) .map_err(|e| Error::CannotParseContractId(wasm_hash.clone(), e))?, ), })]); diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index 3aad487c82..ef443853bc 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -3,17 +3,17 @@ clippy::must_use_candidate, clippy::missing_panics_doc )] +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 rpc; pub mod toid; pub mod utils; pub mod wasm; -use std::path::Path; - pub use commands::Root; pub fn parse_cmd(s: &str) -> Result diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index ff0018a94c..97fc9bfcc7 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -9,7 +9,7 @@ use soroban_env_host::xdr::{ TransactionV1Envelope, WriteXdr, }; -pub mod contract_spec; +pub use soroban_spec_tools::contract as contract_spec; /// # Errors /// @@ -55,16 +55,17 @@ pub fn sign_transaction( /// /// Might return an error pub fn contract_id_from_str(contract_id: &str) -> Result<[u8; 32], stellar_strkey::DecodeError> { - stellar_strkey::Contract::from_string(contract_id) - .map(|strkey| strkey.0) - .or_else(|_| { + Ok( + if let Ok(strkey) = stellar_strkey::Contract::from_string(contract_id) { + strkey.0 + } else { // strkey failed, try to parse it as a hex string, for backwards compatibility. soroban_spec_tools::utils::padded_hex_from_str(contract_id, 32) .map_err(|_| stellar_strkey::DecodeError::Invalid)? .try_into() - .map_err(|_| stellar_strkey::DecodeError::Invalid) - }) - .map_err(|_| stellar_strkey::DecodeError::Invalid) + .map_err(|_| stellar_strkey::DecodeError::Invalid)? + }, + ) } /// # Errors @@ -198,47 +199,5 @@ mod tests { ), Err(err) => panic!("Failed to parse contract id: {err}"), } - - // hex - match contract_id_from_str( - "363eaa3867841fbad0f4ed88c779e4fe66e56a2470dc98c0ec9c073d05c7b103", - ) { - Ok(contract_id) => assert_eq!( - contract_id, - [ - 0x36, 0x3e, 0xaa, 0x38, 0x67, 0x84, 0x1f, 0xba, 0xd0, 0xf4, 0xed, 0x88, 0xc7, - 0x79, 0xe4, 0xfe, 0x66, 0xe5, 0x6a, 0x24, 0x70, 0xdc, 0x98, 0xc0, 0xec, 0x9c, - 0x07, 0x3d, 0x05, 0xc7, 0xb1, 0x03, - ] - ), - Err(err) => panic!("Failed to parse contract id: {err}"), - } - - // unpadded-hex - match contract_id_from_str("1") { - Ok(contract_id) => assert_eq!( - contract_id, - [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - ] - ), - Err(err) => panic!("Failed to parse contract id: {err}"), - } - - // invalid hex - match contract_id_from_str("foobar") { - Ok(_) => panic!("Expected parsing to fail"), - Err(err) => assert_eq!(err, stellar_strkey::DecodeError::Invalid), - } - - // hex too long (33 bytes) - match contract_id_from_str( - "000000000000000000000000000000000000000000000000000000000000000000", - ) { - Ok(_) => panic!("Expected parsing to fail"), - Err(err) => assert_eq!(err, stellar_strkey::DecodeError::Invalid), - } } } diff --git a/cmd/soroban-cli/src/wasm.rs b/cmd/soroban-cli/src/wasm.rs index fce44c7c56..4b8a7f8ca8 100644 --- a/cmd/soroban-cli/src/wasm.rs +++ b/cmd/soroban-cli/src/wasm.rs @@ -1,11 +1,12 @@ use clap::arg; use soroban_env_host::xdr::{self, LedgerKey, LedgerKeyContractCode}; +use soroban_spec_tools::contract::{self, Spec}; use std::{ fs, io, path::{Path, PathBuf}, }; -use crate::utils::{self, contract_spec::ContractSpec}; +use crate::utils::{self}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -25,7 +26,7 @@ pub enum Error { #[error(transparent)] Parser(#[from] wasmparser::BinaryReaderError), #[error(transparent)] - ContractSpec(#[from] crate::utils::contract_spec::Error), + ContractSpec(#[from] contract::Error), } #[derive(Debug, clap::Args, Clone)] @@ -60,9 +61,9 @@ impl Args { /// # Errors /// May fail to read wasm file or parse xdr section - pub fn parse(&self) -> Result { + pub fn parse(&self) -> Result { let contents = self.read()?; - Ok(ContractSpec::new(&contents)?) + Ok(Spec::new(&contents)?) } } diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index 34ab0caa23..e33794a229 100644 --- a/docs/soroban-cli-full-docs.md +++ b/docs/soroban-cli-full-docs.md @@ -110,13 +110,28 @@ Full CLI reference: https://github.com/stellar/soroban-tools/tree/main/docs/soro ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `-f`, `--filter-logs ` — Filter logs output. To turn on "soroban_cli::log::footprint=debug" or off "=off". Can also use env var `RUST_LOG` * `-q`, `--quiet` — Do not write logs to stderr including `INFO` + + Possible values: `true`, `false` + * `-v`, `--verbose` — Log DEBUG events + + Possible values: `true`, `false` + * `--very-verbose` — Log DEBUG and TRACE events + + Possible values: `true`, `false` + * `--list` — List installed plugins. E.g. `soroban-hello` + Possible values: `true`, `false` + + ## `soroban completion` @@ -185,6 +200,9 @@ Add a new network * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -202,6 +220,9 @@ Remove a network ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -215,9 +236,15 @@ List networks ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `-l`, `--long` — Get more info about the networks + Possible values: `true`, `false` + + ## `soroban config identity` @@ -251,8 +278,17 @@ Add a new identity (keypair, ledger, macOS keychain) ###### **Options:** * `--secret-key` — Add using secret_key Can provide with SOROBAN_SECRET_KEY + + Possible values: `true`, `false` + * `--seed-phrase` — Add using 12 word seed phrase to generate secret_key + + Possible values: `true`, `false` + * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -271,6 +307,9 @@ Given an identity return its address (public key) * `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -292,6 +331,9 @@ Fund an identity on a test network * `--network ` — Name of network to use from config * `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -309,12 +351,24 @@ Generate a new identity with a seed phrase, currently 12 words ###### **Options:** * `--no-fund` — Do not fund address + + Possible values: `true`, `false` + * `--seed ` — Optional seed to use when generating seed phrase. Random otherwise * `-s`, `--as-secret` — Output the generated identity as a secret key + + Possible values: `true`, `false` + * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--hd-path ` — When generating a secret key, which hd_path should be used from the original seed_phrase * `-d`, `--default-seed` — Generate the default seed phrase. Useful for testing. Equivalent to --seed 0000000000000000 + + Possible values: `true`, `false` + * `--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 @@ -330,9 +384,15 @@ List identities ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `-l`, `--long` + Possible values: `true`, `false` + + ## `soroban config identity rm` @@ -348,6 +408,9 @@ Remove an identity ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -366,6 +429,9 @@ Given an identity return its private key * `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -423,6 +489,9 @@ Get Id of builtin Soroban Asset Contract. Deprecated, use `soroban contract id a * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -442,6 +511,9 @@ Deploy builtin Soroban Asset Contract * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -498,8 +570,14 @@ Generate a TypeScript / JavaScript package * `--wasm ` — Path to optional wasm binary * `--output-dir ` — Where to place generated project * `--overwrite` — Whether to overwrite output directory if it already exists + + Possible values: `true`, `false` + * `--contract-id ` — The contract ID/address on the network * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server @@ -528,10 +606,19 @@ To view the commands that will be executed, without executing them, use the --pr Default value: `release` * `--features ` — Build with the list of features activated, space or comma separated * `--all-features` — Build with the all features activated + + Possible values: `true`, `false` + * `--no-default-features` — Build with the default feature not activated + + Possible values: `true`, `false` + * `--out-dir ` — Directory to copy wasm files to * `--print-commands-only` — Print commands to build without executing them + Possible values: `true`, `false` + + ## `soroban contract extend` @@ -546,6 +633,9 @@ If no keys are specified the contract itself is extended. * `--ledgers-to-extend ` — Number of ledgers to extend the entries * `--ttl-ledger-only` — Only print the new Time To Live ledger + + Possible values: `true`, `false` + * `--id ` — Contract ID to which owns the data entries. If no keys provided the Contract's instance will be extended * `--key ` — Storage key (symbols only) * `--key-xdr ` — Storage key (base64-encoded XDR) @@ -553,6 +643,8 @@ If no keys are specified the contract itself is extended. * `--wasm-hash ` — Path to Wasm file of contract code to extend * `--durability ` — Storage entry durability + Default value: `persistent` + Possible values: - `persistent`: Persistent @@ -565,6 +657,9 @@ If no keys are specified the contract itself is extended. * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -589,6 +684,9 @@ Deploy a wasm contract * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -597,6 +695,9 @@ Deploy a wasm contract Default value: `false` + Possible values: `true`, `false` + + ## `soroban contract fetch` @@ -610,6 +711,9 @@ Fetch a contract's Wasm binary * `--id ` — Contract ID to fetch * `-o`, `--out-file ` — Where to write output otherwise stdout is used * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server @@ -645,6 +749,9 @@ Deploy builtin Soroban Asset Contract * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -664,6 +771,9 @@ Deploy normal Wasm Contract * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -709,6 +819,9 @@ Inspect a WASM file listing contract functions, meta, etc Pretty print of contract spec entries * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -727,6 +840,9 @@ Install a WASM file to the ledger without creating a contract instance * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -736,6 +852,9 @@ Install a WASM file to the ledger without creating a contract instance Default value: `false` + Possible values: `true`, `false` + + ## `soroban contract invoke` @@ -756,12 +875,23 @@ soroban contract invoke ... -- --help * `--id ` — Contract ID to invoke * `--cost` — Output the cost execution to stderr + + Possible values: `true`, `false` + +* `--instructions ` — Number of instructions to simulate +* `--is-view` — Do not sign and submit transaction + + Possible values: `true`, `false` + * `--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 * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -809,6 +939,8 @@ Print the current value of a contract-data ledger entry * `--wasm-hash ` — Path to Wasm file of contract code to extend * `--durability ` — Storage entry durability + Default value: `persistent` + Possible values: - `persistent`: Persistent @@ -821,6 +953,9 @@ Print the current value of a contract-data ledger entry * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -842,6 +977,8 @@ If no keys are specificed the contract itself is restored. * `--wasm-hash ` — Path to Wasm file of contract code to extend * `--durability ` — Storage entry durability + Default value: `persistent` + Possible values: - `persistent`: Persistent @@ -850,12 +987,18 @@ If no keys are specificed the contract itself is restored. * `--ledgers-to-extend ` — Number of ledgers to extend the entry * `--ttl-ledger-only` — Only print the new Time To Live ledger + + Possible values: `true`, `false` + * `--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 * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -897,6 +1040,9 @@ Watch the network for contract events Possible values: `all`, `contract`, `system` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server @@ -935,8 +1081,17 @@ Add a new identity (keypair, ledger, macOS keychain) ###### **Options:** * `--secret-key` — Add using secret_key Can provide with SOROBAN_SECRET_KEY + + Possible values: `true`, `false` + * `--seed-phrase` — Add using 12 word seed phrase to generate secret_key + + Possible values: `true`, `false` + * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -955,6 +1110,9 @@ Given an identity return its address (public key) * `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -976,6 +1134,9 @@ Fund an identity on a test network * `--network ` — Name of network to use from config * `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -993,12 +1154,24 @@ Generate a new identity with a seed phrase, currently 12 words ###### **Options:** * `--no-fund` — Do not fund address + + Possible values: `true`, `false` + * `--seed ` — Optional seed to use when generating seed phrase. Random otherwise * `-s`, `--as-secret` — Output the generated identity as a secret key + + Possible values: `true`, `false` + * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--hd-path ` — When generating a secret key, which hd_path should be used from the original seed_phrase * `-d`, `--default-seed` — Generate the default seed phrase. Useful for testing. Equivalent to --seed 0000000000000000 + + Possible values: `true`, `false` + * `--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 @@ -1014,9 +1187,15 @@ List identities ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `-l`, `--long` + Possible values: `true`, `false` + + ## `soroban keys rm` @@ -1032,6 +1211,9 @@ Remove an identity ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -1050,6 +1232,9 @@ Given an identity return its private key * `--hd-path ` — If identity is a seed phrase use this hd path, default is 0 * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -1095,6 +1280,9 @@ Deploy a token contract to wrap an existing Stellar classic asset for smart cont * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm @@ -1117,6 +1305,9 @@ Compute the expected contract id for the given asset Deprecated, use `soroban co * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -1296,6 +1487,9 @@ Add a new network * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -1313,6 +1507,9 @@ Remove a network ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." @@ -1326,9 +1523,15 @@ List networks ###### **Options:** * `--global` — Use global config + + Possible values: `true`, `false` + * `--config-dir ` — Location of config directory, default is "." * `-l`, `--long` — Get more info about the networks + Possible values: `true`, `false` + + ## `soroban version`