From 587a229541043ca3b2e3e4514296d6c87625b25c Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Wed, 1 May 2024 17:40:27 -0400 Subject: [PATCH] feat!: remove unneeded logic (#157) * feat!: remove unneeded logic Including signing and ensure that RPC methods are pure * fix: clean up Cargo.toml --- Cargo.lock | 171 ++---------- Cargo.toml | 22 -- cmd/crates/stellar-rpc-client/Cargo.toml | 11 +- cmd/crates/stellar-rpc-client/src/lib.rs | 314 +++++++++++------------ cmd/crates/stellar-rpc-client/src/txn.rs | 293 ++------------------- 5 files changed, 190 insertions(+), 621 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f6aaf96..0c4842ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,18 +199,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" -[[package]] -name = "bytes-lit" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" -dependencies = [ - "num-bigint", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "cc" version = "1.0.83" @@ -379,9 +367,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.6" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c376d08ea6aa96aafe61237c7200d1241cb177b7d3a542d791f2d118e9cbb955" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ "darling_core", "darling_macro", @@ -389,9 +377,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.6" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33043dcd19068b8192064c704b3f83eb464f91f1ff527b44a4e2b08d9cdb8855" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", @@ -403,9 +391,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.6" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", @@ -1016,17 +1004,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "num-bigint" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -1181,16 +1158,6 @@ dependencies = [ "soroban-simulation", ] -[[package]] -name = "prettyplease" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -1441,9 +1408,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ "base64 0.21.7", "chrono", @@ -1459,9 +1426,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" dependencies = [ "darling", "proc-macro2", @@ -1548,7 +1515,6 @@ dependencies = [ "ethnum", "num-derive", "num-traits", - "serde", "soroban-env-macros", "soroban-wasmi", "static_assertions", @@ -1556,16 +1522,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "soroban-env-guest" -version = "21.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f39b60d7a8467e52ffb7863efba4b3ea3947aa028af8d88f4f5a76bb2909d8" -dependencies = [ - "soroban-env-common", - "static_assertions", -] - [[package]] name = "soroban-env-host" version = "21.0.1" @@ -1595,7 +1551,7 @@ dependencies = [ "soroban-env-common", "soroban-wasmi", "static_assertions", - "stellar-strkey 0.0.8", + "stellar-strkey", "wasmparser", ] @@ -1614,57 +1570,6 @@ dependencies = [ "syn", ] -[[package]] -name = "soroban-ledger-snapshot" -version = "21.0.1-preview.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b71878a8a3db38d5da6fa42d48d055b7937ef9ae608ffcb23f9589aa7989f10" -dependencies = [ - "serde", - "serde_json", - "serde_with", - "soroban-env-common", - "soroban-env-host", - "thiserror", -] - -[[package]] -name = "soroban-sdk" -version = "21.0.1-preview.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85dc6c199238c4150027034e3cfc383d9c64274292c8dc19bae598e843431356" -dependencies = [ - "bytes-lit", - "rand", - "serde", - "serde_json", - "soroban-env-guest", - "soroban-env-host", - "soroban-ledger-snapshot", - "soroban-sdk-macros", - "stellar-strkey 0.0.8", -] - -[[package]] -name = "soroban-sdk-macros" -version = "21.0.1-preview.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c503bf0d43499884aa22877e7f293b19882c2088dcd3f00b01eb21b4c65001" -dependencies = [ - "crate-git-revision", - "darling", - "itertools 0.11.0", - "proc-macro2", - "quote", - "rustc_version", - "sha2", - "soroban-env-common", - "soroban-spec", - "soroban-spec-rust", - "stellar-xdr", - "syn", -] - [[package]] name = "soroban-simulation" version = "21.0.1" @@ -1678,34 +1583,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "soroban-spec" -version = "21.0.1-preview.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4138300450ad75817954070b1cf32422b236410f937205e9f47e44114fd82fe9" -dependencies = [ - "base64 0.13.1", - "stellar-xdr", - "thiserror", - "wasmparser", -] - -[[package]] -name = "soroban-spec-rust" -version = "21.0.1-preview.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b230255799160fbcf36986f96f506cca671d96541f015546e9f74e99efdedb6" -dependencies = [ - "prettyplease", - "proc-macro2", - "quote", - "sha2", - "soroban-spec", - "stellar-xdr", - "syn", - "thiserror", -] - [[package]] name = "soroban-wasmi" version = "0.31.1-soroban.20.0.1" @@ -1746,7 +1623,6 @@ name = "stellar-rpc-client" version = "21.0.1" dependencies = [ "clap", - "ed25519-dalek", "hex", "http 1.0.0", "itertools 0.10.5", @@ -1756,10 +1632,7 @@ dependencies = [ "serde-aux", "serde_json", "sha2", - "soroban-env-host", - "soroban-sdk", - "soroban-spec", - "stellar-strkey 0.0.7", + "stellar-strkey", "stellar-xdr", "termcolor", "termcolor_output", @@ -1768,16 +1641,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "stellar-strkey" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0689070126ca7f2effc2c5726584446db52190f0cef043c02eb4040a711c11" -dependencies = [ - "base32", - "thiserror", -] - [[package]] name = "stellar-strkey" version = "0.0.8" @@ -1802,7 +1665,7 @@ dependencies = [ "hex", "serde", "serde_with", - "stellar-strkey 0.0.8", + "stellar-strkey", ] [[package]] @@ -1881,9 +1744,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -1902,9 +1765,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index ac12d59c..a0e8b42f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ members = [ "cmd/soroban-rpc/lib/preflight", ] default-members = ["cmd/crates/stellar-rpc-client"] -#exclude = ["cmd/crates/soroban-test/tests/fixtures/hello"] [workspace.package] version = "21.0.1" @@ -13,33 +12,12 @@ rust-version = "1.74.0" [workspace.dependencies.soroban-env-host] version = "=21.0.1" -# git = "https://github.com/stellar/rs-soroban-env" -# rev = "27897f6073aec5241d3486690a33b22c80dd0718" -# path = "../rs-soroban-env/soroban-env-host" [workspace.dependencies.soroban-simulation] version = "=21.0.1" -# git = "https://github.com/stellar/rs-soroban-env" -# rev = "27897f6073aec5241d3486690a33b22c80dd0718" -# path = "../rs-soroban-env/soroban-simulation" - -[workspace.dependencies.soroban-spec] -version = "=21.0.1-preview.1" -# git = "https://github.com/stellar/rs-soroban-sdk" -# rev = "c30bc769e379bef9b94a3ceb464aa78c1185eeb3" -# path = "../rs-soroban-sdk/soroban-spec" - -[workspace.dependencies.soroban-sdk] -version = "=21.0.1-preview.1" -# git = "https://github.com/stellar/rs-soroban-sdk" -# rev = "c30bc769e379bef9b94a3ceb464aa78c1185eeb3" - [workspace.dependencies.stellar-xdr] version = "=21.0.1" -default-features = true -# git = "https://github.com/stellar/rs-stellar-xdr" -# rev = "a80c899c61e869fd00b7b475a4947ab6aaf9dcac" [workspace.dependencies] base64 = "0.22.0" diff --git a/cmd/crates/stellar-rpc-client/Cargo.toml b/cmd/crates/stellar-rpc-client/Cargo.toml index 419e8657..05b40b98 100644 --- a/cmd/crates/stellar-rpc-client/Cargo.toml +++ b/cmd/crates/stellar-rpc-client/Cargo.toml @@ -17,15 +17,13 @@ crate-type = ["rlib"] [dependencies] -soroban-sdk = { workspace = true } -soroban-env-host = { workspace = true } -stellar-strkey = "0.0.7" -stellar-xdr = { workspace = true, features = ["curr", "std", "serde"] } -soroban-spec = { workspace = true } +stellar-strkey = "0.0.8" +stellar-xdr = { workspace = true, features = ["curr", "std", "serde", "base64"] } + termcolor = "1.1.3" termcolor_output = "1.0.1" -clap = { version = "4.1.8", features = ["derive", "env", "deprecated", "string"] } +clap = { version = "4.1.8", features = ["derive"] } serde_json = "1.0.82" serde-aux = "4.1.2" itertools = "0.10.0" @@ -34,7 +32,6 @@ 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 diff --git a/cmd/crates/stellar-rpc-client/src/lib.rs b/cmd/crates/stellar-rpc-client/src/lib.rs index ca861e1b..685914ed 100644 --- a/cmd/crates/stellar-rpc-client/src/lib.rs +++ b/cmd/crates/stellar-rpc-client/src/lib.rs @@ -7,21 +7,22 @@ use serde_aux::prelude::{ deserialize_default_from_null, deserialize_number_from_string, deserialize_option_number_from_string, }; -use soroban_env_host::xdr::{ - self, AccountEntry, AccountId, ContractDataEntry, DiagnosticEvent, Error as XdrError, - LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, Limited, PublicKey, ReadXdr, - SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData, Transaction, - TransactionEnvelope, TransactionMeta, TransactionMetaV3, TransactionResult, Uint256, VecM, - WriteXdr, +use stellar_xdr::curr::{ + self as xdr, AccountEntry, AccountId, ContractDataEntry, ContractEventType, DiagnosticEvent, + Error as XdrError, Hash, LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, + Limited, Limits, PublicKey, ReadXdr, ScContractInstance, SorobanAuthorizationEntry, + SorobanResources, SorobanTransactionData, Transaction, TransactionEnvelope, TransactionMeta, + TransactionMetaV3, TransactionResult, TransactionV1Envelope, Uint256, VecM, WriteXdr, }; -use soroban_sdk::token; -use soroban_sdk::xdr::Limits; + use std::{ + f64::consts::E, fmt::Display, str::FromStr, + sync::Arc, time::{Duration, Instant}, }; -use stellar_xdr::curr::ContractEventType; + use termcolor::{Color, ColorChoice, StandardStream, WriteColor}; use termcolor_output::colored; use tokio::time::sleep; @@ -58,7 +59,7 @@ pub enum Error { InvalidRpcUrlFromUriParts(http::uri::InvalidUriParts), #[error("invalid friendbot url: {0}")] InvalidUrl(String), - #[error("jsonrpc error: {0}")] + #[error(transparent)] JsonRpc(#[from] jsonrpsee_core::Error), #[error("json decoding error: {0}")] Serde(#[from] serde_json::Error), @@ -90,12 +91,10 @@ pub enum Error { UnsupportedOperationType, #[error("unexpected contract code data type: {0:?}")] UnexpectedContractCodeDataType(LedgerEntryData), + #[error("unexpected contract instance type: {0:?}")] + UnexpectedContractInstance(xdr::ScVal), #[error("unexpected contract code got token")] UnexpectedToken(ContractDataEntry), - #[error(transparent)] - Spec(#[from] soroban_spec::read::FromWasmError), - #[error(transparent)] - SpecBase64(#[from] soroban_spec::read::ParseSpecBase64Error), #[error("Fee was too large {0}")] LargeFee(u64), #[error("Cannot authorize raw transactions")] @@ -567,9 +566,11 @@ pub struct FullLedgerEntries { pub latest_ledger: i64, } +#[derive(Debug, Clone)] pub struct Client { - base_url: String, + base_url: Arc, timeout_in_secs: u64, + http_client: Arc, } impl Client { @@ -599,13 +600,33 @@ impl Client { } } let uri = Uri::from_parts(parts).map_err(Error::InvalidRpcUrlFromUriParts)?; + let base_url = Arc::from(uri.to_string()); tracing::trace!(?uri); + let mut headers = HeaderMap::new(); + headers.insert("X-Client-Name", unsafe { + "soroban-cli".parse().unwrap_unchecked() + }); + let version = VERSION.unwrap_or("devel"); + headers.insert("X-Client-Version", unsafe { + version.parse().unwrap_unchecked() + }); + let http_client = Arc::new( + HttpClientBuilder::default() + .set_headers(headers) + .build(&base_url)?, + ); Ok(Self { - base_url: uri.to_string(), + base_url, timeout_in_secs: 30, + http_client, }) } + #[must_use] + pub fn base_url(&self) -> &str { + &self.base_url + } + /// Create a new client with a timeout in seconds /// # Errors pub fn new_with_timeout(base_url: &str, timeout: u64) -> Result { @@ -614,17 +635,9 @@ impl Client { Ok(client) } - /// - /// # Errors - fn client(&self) -> Result { - let url = self.base_url.clone(); - let mut headers = HeaderMap::new(); - headers.insert("X-Client-Name", "soroban-cli".parse().unwrap()); - let version = VERSION.unwrap_or("devel"); - headers.insert("X-Client-Version", version.parse().unwrap()); - Ok(HttpClientBuilder::default() - .set_headers(headers) - .build(url)?) + #[must_use] + pub fn client(&self) -> &HttpClient { + &self.http_client } /// @@ -659,7 +672,7 @@ impl Client { pub async fn get_network(&self) -> Result { tracing::trace!("Getting network"); Ok(self - .client()? + .client() .request("getNetwork", ObjectParams::new()) .await?) } @@ -669,7 +682,7 @@ impl Client { pub async fn get_latest_ledger(&self) -> Result { tracing::trace!("Getting latest ledger"); Ok(self - .client()? + .client() .request("getLatestLedger", ObjectParams::new()) .await?) } @@ -687,15 +700,7 @@ impl Client { let response = self.get_ledger_entries(&keys).await?; let entries = response.entries.unwrap_or_default(); if entries.is_empty() { - return Err(Error::NotFound( - "Account".to_string(), - format!( - r#"{address} -Might need to fund account like: -soroban config identity fund {address} --network -soroban config identity fund {address} --helper-url "# - ), - )); + return Err(Error::NotFound("Account".to_string(), address.to_owned())); } let ledger_entry = &entries[0]; let mut read = Limited::new(ledger_entry.xdr.as_bytes(), Limits::none()); @@ -707,13 +712,9 @@ soroban config identity fund {address} --helper-url "# } } - /// + /// Send a transaction to the network and get back the hash of the transaction. /// # Errors - pub async fn send_transaction( - &self, - tx: &TransactionEnvelope, - ) -> Result { - let client = self.client()?; + pub async fn send_transaction(&self, tx: &TransactionEnvelope) -> Result { tracing::trace!("Sending:\n{tx:#?}"); let mut oparams = ObjectParams::new(); oparams.insert("transaction", tx.to_xdr_base64(Limits::none())?)?; @@ -722,7 +723,8 @@ soroban config identity fund {address} --helper-url "# error_result_xdr, status, .. - } = client + } = self + .client() .request("sendTransaction", oparams) .await .map_err(|err| { @@ -740,61 +742,38 @@ soroban config identity fund {address} --helper-url "# .map_err(|_| Error::InvalidResponse) }) .map(|r| r.result); - tracing::error!("TXN failed:\n {error:#?}"); + tracing::error!("TXN {hash} failed:\n {error:#?}"); return Err(Error::TransactionSubmissionFailed(format!("{:#?}", error?))); } - // even if status == "success" we need to query the transaction status in order to get the result - - // Poll the transaction status - let start = Instant::now(); - loop { - let response: GetTransactionResponse = self.get_transaction(&hash).await?.try_into()?; - match response.status.as_str() { - "SUCCESS" => { - // TODO: the caller should probably be printing this - tracing::trace!("{response:#?}"); - return Ok(response); - } - "FAILED" => { - tracing::error!("{response:#?}"); - // TODO: provide a more elaborate error - return Err(Error::TransactionSubmissionFailed(format!( - "{:#?}", - response.result - ))); - } - "NOT_FOUND" => (), - _ => { - return Err(Error::UnexpectedTransactionStatus(response.status)); - } - }; - let duration = start.elapsed(); - if duration.as_secs() > self.timeout_in_secs { - return Err(Error::TransactionSubmissionTimeout); - } - sleep(Duration::from_secs(1)).await; - } + Ok(Hash::from_str(&hash)?) } /// /// # Errors - pub async fn simulate_transaction( + pub async fn send_transaction_polling( &self, tx: &TransactionEnvelope, - ) -> Result { - tracing::trace!("Simulating:\n{tx:#?}"); - let base64_tx = tx.to_xdr_base64(Limits::none())?; - let mut oparams = ObjectParams::new(); - oparams.insert("transaction", base64_tx)?; - let response: SimulateTransactionResponse = self - .client()? - .request("simulateTransaction", oparams) + ) -> Result { + let hash = self.send_transaction(tx).await?; + self.get_transaction_polling(&hash, None).await + } + + /// + /// # Errors + pub async fn simulate_and_assemble_transaction( + &self, + tx: &Transaction, + ) -> Result { + let sim_res = self + .simulate_transaction_envelope(&TransactionEnvelope::Tx(TransactionV1Envelope { + tx: tx.clone(), + signatures: VecM::default(), + })) .await?; - tracing::trace!("Simulation response:\n {response:#?}"); - match response.error { - None => Ok(response), + match sim_res.error { + None => Ok(Assembled::new(tx, sim_res)?), Some(e) => { - log::diagnostic_events(&response.events, tracing::Level::ERROR); + log::diagnostic_events(&sim_res.events, tracing::Level::ERROR); Err(Error::TransactionSimulationFailed(e)) } } @@ -802,65 +781,78 @@ soroban config identity fund {address} --helper-url "# /// /// # Errors - pub async fn send_assembled_transaction( + pub async fn simulate_transaction_envelope( &self, - txn: txn::Assembled, - source_key: &ed25519_dalek::SigningKey, - signers: &[ed25519_dalek::SigningKey], - network_passphrase: &str, - log_events: Option, - log_resources: Option, - ) -> 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) + tx: &TransactionEnvelope, + ) -> Result { + tracing::trace!("Simulating:\n{tx:#?}"); + let base64_tx = tx.to_xdr_base64(Limits::none())?; + let mut oparams = ObjectParams::new(); + oparams.insert("transaction", base64_tx)?; + let sim_res = self + .client() + .request("simulateTransaction", oparams) .await?; - authorized.log(log_events, log_resources)?; - - let tx = authorized.sign(source_key, network_passphrase)?; - self.send_transaction(&tx).await + tracing::trace!("Simulation response:\n {sim_res:#?}"); + Ok(sim_res) } /// /// # 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 + pub async fn get_transaction(&self, tx_id: &Hash) -> Result { + let mut oparams = ObjectParams::new(); + oparams.insert("hash", tx_id)?; + Ok(self.client().request("getTransaction", oparams).await?) } + /// Poll the transaction status. Can provide a timeout in seconds, otherwise uses the default timeout. /// - /// # Errors - pub async fn create_assembled_transaction( - &self, - txn: &Transaction, - ) -> Result { - txn::Assembled::new(txn, self).await - } - + /// It uses exponential backoff with a base of 1 second and a maximum of 30 seconds. /// /// # Errors - pub async fn get_transaction(&self, tx_id: &str) -> Result { - let mut oparams = ObjectParams::new(); - oparams.insert("hash", tx_id)?; - Ok(self.client()?.request("getTransaction", oparams).await?) + /// - `Error::TransactionSubmissionTimeout` if the transaction status is not found within the timeout + /// - `Error::TransactionSubmissionFailed` if the transaction status is "FAILED" + /// - `Error::UnexpectedTransactionStatus` if the transaction status is not one of "SUCCESS", "FAILED", or ``NOT_FOUND`` + /// - `json_rpsee` Errors + pub async fn get_transaction_polling( + &self, + tx_id: &Hash, + timeout_s: Option, + ) -> Result { + // Poll the transaction status + let start = Instant::now(); + let timeout = timeout_s.unwrap_or(Duration::from_secs(self.timeout_in_secs)); + // see https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=50731 + // Is optimimal exponent for expontial backoff + let exponential_backoff: f64 = 1.0 / (1.0 - E.powf(-1.0)); + let mut sleep_time = Duration::from_secs(1); + loop { + let response = self.get_transaction(tx_id).await?; + match response.status.as_str() { + "SUCCESS" => { + // TODO: the caller should probably be printing this + tracing::trace!("{response:#?}"); + return Ok(response); + } + "FAILED" => { + tracing::error!("{response:#?}"); + // TODO: provide a more elaborate error + return Err(Error::TransactionSubmissionFailed(format!( + "{:#?}", + response.result + ))); + } + "NOT_FOUND" => (), + _ => { + return Err(Error::UnexpectedTransactionStatus(response.status)); + } + }; + if start.elapsed() > timeout { + return Err(Error::TransactionSubmissionTimeout); + } + sleep(sleep_time).await; + sleep_time = Duration::from_secs_f64(sleep_time.as_secs_f64() * exponential_backoff); + } } /// @@ -879,7 +871,7 @@ soroban config identity fund {address} --helper-url "# } let mut oparams = ObjectParams::new(); oparams.insert("keys", base64_keys)?; - Ok(self.client()?.request("getLedgerEntries", oparams).await?) + Ok(self.client().request("getLedgerEntries", oparams).await?) } /// @@ -962,7 +954,7 @@ soroban config identity fund {address} --helper-url "# oparams.insert("filters", vec![filters])?; oparams.insert("pagination", pagination)?; - Ok(self.client()?.request("getEvents", oparams).await?) + Ok(self.client().request("getEvents", oparams).await?) } /// @@ -1024,27 +1016,19 @@ soroban config identity fund {address} --helper-url "# scval => Err(Error::UnexpectedContractCodeDataType(scval)), } } + + /// Get the contract instance from the network. Could be normal contract or native Stellar Asset Contract (SAC) /// /// # Errors - pub async fn get_remote_contract_spec( + /// - Could fail to find contract or have a network error + pub async fn get_contract_instance( &self, contract_id: &[u8; 32], - ) -> Result, Error> { + ) -> Result { let contract_data = self.get_contract_data(contract_id).await?; match contract_data.val { - xdr::ScVal::ContractInstance(xdr::ScContractInstance { - executable: xdr::ContractExecutable::Wasm(hash), - .. - }) => Ok(soroban_spec::read::from_wasm( - &self.get_remote_wasm_from_hash(hash).await?, - )?), - xdr::ScVal::ContractInstance(xdr::ScContractInstance { - executable: xdr::ContractExecutable::StellarAsset, - .. - }) => Ok(soroban_spec::read::parse_raw( - &token::StellarAssetSpec::spec_xdr(), - )?), - _ => Err(Error::Xdr(XdrError::Invalid)), + xdr::ScVal::ContractInstance(instance) => Ok(instance), + scval => Err(Error::UnexpectedContractInstance(scval)), } } } @@ -1122,29 +1106,29 @@ mod tests { fn test_rpc_url_default_ports() { // Default ports are added. let client = Client::new("http://example.com").unwrap(); - assert_eq!(client.base_url, "http://example.com:80/"); + assert_eq!(client.base_url(), "http://example.com:80/"); let client = Client::new("https://example.com").unwrap(); - assert_eq!(client.base_url, "https://example.com:443/"); + assert_eq!(client.base_url(), "https://example.com:443/"); // Ports are not added when already present. let client = Client::new("http://example.com:8080").unwrap(); - assert_eq!(client.base_url, "http://example.com:8080/"); + assert_eq!(client.base_url(), "http://example.com:8080/"); let client = Client::new("https://example.com:8080").unwrap(); - assert_eq!(client.base_url, "https://example.com:8080/"); + assert_eq!(client.base_url(), "https://example.com:8080/"); // Paths are not modified. let client = Client::new("http://example.com/a/b/c").unwrap(); - assert_eq!(client.base_url, "http://example.com:80/a/b/c"); + assert_eq!(client.base_url(), "http://example.com:80/a/b/c"); let client = Client::new("https://example.com/a/b/c").unwrap(); - assert_eq!(client.base_url, "https://example.com:443/a/b/c"); + assert_eq!(client.base_url(), "https://example.com:443/a/b/c"); let client = Client::new("http://example.com/a/b/c/").unwrap(); - assert_eq!(client.base_url, "http://example.com:80/a/b/c/"); + assert_eq!(client.base_url(), "http://example.com:80/a/b/c/"); let client = Client::new("https://example.com/a/b/c/").unwrap(); - assert_eq!(client.base_url, "https://example.com:443/a/b/c/"); + assert_eq!(client.base_url(), "https://example.com:443/a/b/c/"); let client = Client::new("http://example.com/a/b:80/c/").unwrap(); - assert_eq!(client.base_url, "http://example.com:80/a/b:80/c/"); + assert_eq!(client.base_url(), "http://example.com:80/a/b:80/c/"); let client = Client::new("https://example.com/a/b:80/c/").unwrap(); - assert_eq!(client.base_url, "https://example.com:443/a/b:80/c/"); + assert_eq!(client.base_url(), "https://example.com:443/a/b:80/c/"); } #[test] diff --git a/cmd/crates/stellar-rpc-client/src/txn.rs b/cmd/crates/stellar-rpc-client/src/txn.rs index 4fc898b6..f80202b7 100644 --- a/cmd/crates/stellar-rpc-client/src/txn.rs +++ b/cmd/crates/stellar-rpc-client/src/txn.rs @@ -1,23 +1,20 @@ -use ed25519_dalek::Signer; +// use ed25519_dalek::Signer; use sha2::{Digest, Sha256}; -use soroban_env_host::xdr::{ - self, AccountId, DecoratedSignature, ExtensionPoint, Hash, HashIdPreimage, - 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 stellar_xdr::curr::{ + self as xdr, ExtensionPoint, Hash, InvokeHostFunctionOp, LedgerFootprint, Limits, Memo, + Operation, OperationBody, Preconditions, ReadXdr, RestoreFootprintOp, + SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanResources, SorobanTransactionData, + Transaction, TransactionExt, TransactionSignaturePayload, + TransactionSignaturePayloadTaggedTransaction, VecM, WriteXdr, }; -use super::{Client, Error, RestorePreamble, SimulateTransactionResponse}; +use super::{Error, RestorePreamble, SimulateTransactionResponse}; use super::{LogEvents, LogResources}; pub struct Assembled { - txn: Transaction, - sim_res: SimulateTransactionResponse, + pub(crate) txn: Transaction, + pub(crate) sim_res: SimulateTransactionResponse, } /// Represents an assembled transaction ready to be signed and submitted to the network. @@ -33,8 +30,7 @@ impl Assembled { /// # 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?; + pub fn new(txn: &Transaction, sim_res: SimulateTransactionResponse) -> Result { let txn = assemble(txn, &sim_res)?; Ok(Self { txn, sim_res }) } @@ -57,90 +53,14 @@ 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.transaction(); - let tx_hash = self.hash(network_passphrase)?; - let tx_signature = key.sign(&tx_hash); - - let decorated_signature = DecoratedSignature { - hint: SignatureHint(key.verifying_key().to_bytes()[28..].try_into()?), - signature: Signature(tx_signature.to_bytes().try_into()?), - }; - - Ok(TransactionEnvelope::Tx(TransactionV1Envelope { - tx: tx.clone(), - signatures: vec![decorated_signature].try_into()?, - })) - } - - /// - /// Simulates the assembled transaction. - /// - /// # Arguments - /// - /// * `tx` - The original transaction. - /// * `client` - The client used for simulation. + /// Create a transaction for restoring any data in the `restore_preamble` field of the `SimulateTransactionResponse`. /// /// # Errors - /// - /// Returns an error if simulation fails. - pub async fn simulate( - tx: &Transaction, - client: &Client, - ) -> Result { - client - .simulate_transaction(&TransactionEnvelope::Tx(TransactionV1Envelope { - tx: tx.clone(), - signatures: VecM::default(), - })) - .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, - source_key: &ed25519_dalek::SigningKey, - network_passphrase: &str, - ) -> Result { + pub fn restore_txn(&self) -> Result, Error> { if let Some(restore_preamble) = &self.sim_res.restore_preamble { - // Build and submit the restore transaction - client - .send_transaction( - &Assembled::new(&restore(self.transaction(), restore_preamble)?, client) - .await? - .sign(source_key, network_passphrase)?, - ) - .await?; - Ok(self.bump_seq_num()) + restore(self.transaction(), restore_preamble).map(Option::Some) } else { - Ok(self) + Ok(None) } } @@ -156,29 +76,6 @@ impl Assembled { &self.sim_res } - /// - /// # Errors - pub async fn authorize( - self, - client: &Client, - source_key: &ed25519_dalek::SigningKey, - signers: &[ed25519_dalek::SigningKey], - seq_num: u32, - network_passphrase: &str, - ) -> Result { - if let Some(txn) = sign_soroban_authorizations( - self.transaction(), - source_key, - signers, - seq_num, - network_passphrase, - )? { - Self::new(&txn, client).await - } else { - Ok(self) - } - } - #[must_use] pub fn bump_seq_num(mut self) -> Self { self.txn.seq_num.0 += 1; @@ -268,7 +165,7 @@ impl Assembled { // submission to the network. /// /// # Errors -pub fn assemble( +fn assemble( raw: &Transaction, simulation: &SimulateTransactionResponse, ) -> Result { @@ -342,157 +239,7 @@ fn requires_auth(txn: &Transaction) -> Option { .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( - raw: &Transaction, - source_key: &ed25519_dalek::SigningKey, - signers: &[ed25519_dalek::SigningKey], - signature_expiration_ledger: u32, - network_passphrase: &str, -) -> Result, Error> { - let mut tx = raw.clone(); - let Some(mut op) = requires_auth(&tx) else { - return Ok(None); - }; - - let Operation { - body: OperationBody::InvokeHostFunction(ref mut body), - .. - } = op - else { - return Ok(None); - }; - - let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); - - let verification_key = source_key.verifying_key(); - let source_address = verification_key.as_bytes(); - - let signed_auths = body - .auth - .as_slice() - .iter() - .map(|raw_auth| { - let mut auth = raw_auth.clone(); - let SorobanAuthorizationEntry { - credentials: SorobanCredentials::Address(ref mut credentials), - .. - } = auth - else { - // Doesn't need special signing - return Ok(auth); - }; - let SorobanAddressCredentials { ref address, .. } = credentials; - - // 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(), - }); - } - }; - let signer = if let Some(s) = signers - .iter() - .find(|s| needle == s.verifying_key().as_bytes()) - { - s - } else if needle == source_address { - // This is the source address, so we can sign it - source_key - } else { - // We don't have a signer for this address - return Err(Error::MissingSignerForAddress { - address: stellar_strkey::Strkey::PublicKeyEd25519( - stellar_strkey::ed25519::PublicKey(*needle), - ) - .to_string(), - }); - }; - - sign_soroban_authorization_entry( - raw_auth, - signer, - signature_expiration_ledger, - &network_id, - ) - }) - .collect::, Error>>()?; - - body.auth = signed_auths.try_into()?; - tx.operations = vec![op].try_into()?; - Ok(Some(tx)) -} - -fn sign_soroban_authorization_entry( - raw: &SorobanAuthorizationEntry, - signer: &ed25519_dalek::SigningKey, - signature_expiration_ledger: u32, - network_id: &Hash, -) -> Result { - let mut auth = raw.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: network_id.clone(), - invocation: auth.root_invocation.clone(), - nonce: *nonce, - signature_expiration_ledger, - }) - .to_xdr(Limits::none())?; - - let payload = Sha256::digest(preimage); - 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) -} - -/// -/// # Errors -pub fn restore(parent: &Transaction, restore: &RestorePreamble) -> Result { +fn restore(parent: &Transaction, restore: &RestorePreamble) -> Result { let transaction_data = SorobanTransactionData::from_xdr_base64(&restore.transaction_data, Limits::none())?; let fee = u32::try_from(restore.min_resource_fee) @@ -522,14 +269,14 @@ mod tests { use super::*; use super::super::SimulateHostFunctionResultRaw; - use soroban_env_host::xdr::{ - self, AccountId, ChangeTrustAsset, ChangeTrustOp, ExtensionPoint, Hash, HostFunction, + use stellar_strkey::ed25519::PublicKey as Ed25519PublicKey; + use stellar_xdr::curr::{ + AccountId, ChangeTrustAsset, ChangeTrustOp, ExtensionPoint, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, LedgerFootprint, Memo, MuxedAccount, Operation, Preconditions, PublicKey, ScAddress, ScSymbol, ScVal, SequenceNumber, SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanResources, SorobanTransactionData, Uint256, WriteXdr, }; - use stellar_strkey::ed25519::PublicKey as Ed25519PublicKey; const SOURCE: &str = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI";