From 6f02b79427353072cf6254378246cc36174a81c9 Mon Sep 17 00:00:00 2001 From: "Tyler.S" Date: Thu, 8 Feb 2024 14:03:41 -0800 Subject: [PATCH] Use soroban-rpc crate from the RPC repo --- Cargo.lock | 26 +- Cargo.toml | 5 +- cmd/crates/soroban-rpc/Cargo.toml | 51 - cmd/crates/soroban-rpc/README.md | 3 - .../src/fixtures/event_response.json | 39 - cmd/crates/soroban-rpc/src/lib.rs | 1253 ----------------- cmd/crates/soroban-rpc/src/log.rs | 2 - .../soroban-rpc/src/log/diagnostic_events.rs | 11 - cmd/crates/soroban-rpc/src/txn.rs | 724 ---------- 9 files changed, 25 insertions(+), 2089 deletions(-) delete mode 100644 cmd/crates/soroban-rpc/Cargo.toml delete mode 100644 cmd/crates/soroban-rpc/README.md delete mode 100644 cmd/crates/soroban-rpc/src/fixtures/event_response.json delete mode 100644 cmd/crates/soroban-rpc/src/lib.rs delete mode 100644 cmd/crates/soroban-rpc/src/log.rs delete mode 100644 cmd/crates/soroban-rpc/src/log/diagnostic_events.rs delete mode 100644 cmd/crates/soroban-rpc/src/txn.rs diff --git a/Cargo.lock b/Cargo.lock index e297faa4b..7a2312dcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3525,7 +3525,7 @@ dependencies = [ "soroban-spec", "soroban-spec-json", "soroban-spec-rust", - "soroban-spec-tools", + "soroban-spec-tools 20.3.0", "soroban-spec-typescript", "stellar-strkey 0.0.7", "stellar-xdr", @@ -3639,6 +3639,7 @@ dependencies = [ [[package]] name = "soroban-rpc" version = "20.3.0" +source = "git+https://github.com/stellar/soroban-rpc?rev=b4c2dec162b14b560ed86b9d76a936b7bafb0922#b4c2dec162b14b560ed86b9d76a936b7bafb0922" dependencies = [ "base64 0.21.5", "clap", @@ -3656,7 +3657,7 @@ dependencies = [ "soroban-env-host", "soroban-sdk", "soroban-spec", - "soroban-spec-tools", + "soroban-spec-tools 20.3.0 (git+https://github.com/stellar/soroban-tools?rev=a1e51d263df80682a3dab2b00738700c7326f872)", "stellar-strkey 0.0.7", "stellar-xdr", "termcolor", @@ -3665,7 +3666,6 @@ dependencies = [ "tokio", "tracing", "wasmparser 0.90.0", - "which", ] [[package]] @@ -3782,6 +3782,24 @@ dependencies = [ "which", ] +[[package]] +name = "soroban-spec-tools" +version = "20.3.0" +source = "git+https://github.com/stellar/soroban-tools?rev=a1e51d263df80682a3dab2b00738700c7326f872#a1e51d263df80682a3dab2b00738700c7326f872" +dependencies = [ + "base64 0.21.5", + "ethnum", + "hex", + "itertools 0.10.5", + "serde_json", + "soroban-env-host", + "soroban-spec", + "stellar-strkey 0.0.7", + "stellar-xdr", + "thiserror", + "wasmparser 0.90.0", +] + [[package]] name = "soroban-spec-typescript" version = "20.3.0" @@ -3819,7 +3837,7 @@ dependencies = [ "soroban-ledger-snapshot", "soroban-sdk", "soroban-spec", - "soroban-spec-tools", + "soroban-spec-tools 20.3.0", "stellar-strkey 0.0.7", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 3e55a7e8d..06340e499 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,8 +70,9 @@ version = "20.3.0" path = "cmd/soroban-cli" [workspace.dependencies.soroban-rpc] -version = "20.2.0" -path = "cmd/crates/soroban-rpc" +version = "20.3.0" +git = "https://github.com/stellar/soroban-rpc" +rev = "b4c2dec162b14b560ed86b9d76a936b7bafb0922" [workspace.dependencies.stellar-xdr] version = "=20.1.0" diff --git a/cmd/crates/soroban-rpc/Cargo.toml b/cmd/crates/soroban-rpc/Cargo.toml deleted file mode 100644 index bbe126cc8..000000000 --- a/cmd/crates/soroban-rpc/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[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 = { workspace = true } -soroban-spec-tools = { version = "20.2.0", path = "../soroban-spec-tools" } -soroban-env-host = { workspace = true } -stellar-strkey = "0.0.7" -stellar-xdr = { version = "=20.1.0", default-features = true, features = ["curr", "std", "serde"] } -soroban-spec = { workspace = true } - -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 deleted file mode 100644 index 9185b7fd0..000000000 --- a/cmd/crates/soroban-rpc/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# soroban-rpc - -Tools and utilities for soroban rpc. diff --git a/cmd/crates/soroban-rpc/src/fixtures/event_response.json b/cmd/crates/soroban-rpc/src/fixtures/event_response.json deleted file mode 100644 index 6f520fdfd..000000000 --- a/cmd/crates/soroban-rpc/src/fixtures/event_response.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "events": [{ - "eventType": "system", - "ledger": "43601283", - "ledgerClosedAt": "2022-11-16T16:10:41Z", - "contractId": "CDR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OO5Z", - "id": "0164090849041387521-0000000003", - "pagingToken": "164090849041387521-3", - "topic": [ - "AAAABQAAAAh0cmFuc2Zlcg==", - "AAAAAQB6Mcc=" - ], - "value": "AAAABQAAAApHaWJNb255UGxzAAA=" - }, { - "eventType": "contract", - "ledger": "43601284", - "ledgerClosedAt": "2022-11-16T16:10:41Z", - "contractId": "CDR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OO5Z", - "id": "0164090849041387521-0000000003", - "pagingToken": "164090849041387521-3", - "topic": [ - "AAAABQAAAAh0cmFuc2Zlcg==", - "AAAAAQB6Mcc=" - ], - "value": "AAAABQAAAApHaWJNb255UGxzAAA=" - }, { - "eventType": "system", - "ledger": "43601285", - "ledgerClosedAt": "2022-11-16T16:10:41Z", - "contractId": "CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2", - "id": "0164090849041387521-0000000003", - "pagingToken": "164090849041387521-3", - "topic": [ - "AAAABQAAAAh0cmFuc2Zlcg==", - "AAAAAQB6Mcc=" - ], - "value": "AAAABQAAAApHaWJNb255UGxzAAA=" - }] -} \ No newline at end of file diff --git a/cmd/crates/soroban-rpc/src/lib.rs b/cmd/crates/soroban-rpc/src/lib.rs deleted file mode 100644 index 95c3692e1..000000000 --- a/cmd/crates/soroban-rpc/src/lib.rs +++ /dev/null @@ -1,1253 +0,0 @@ -use http::{uri::Authority, Uri}; -use itertools::Itertools; -use jsonrpsee_core::params::ObjectParams; -use jsonrpsee_core::{self, client::ClientT, rpc_params}; -use jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder}; -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 soroban_sdk::token; -use soroban_sdk::xdr::Limits; -use std::{ - fmt::Display, - str::FromStr, - time::{Duration, Instant}, -}; -use stellar_xdr::curr::ContractEventType; -use termcolor::{Color, ColorChoice, StandardStream, WriteColor}; -use termcolor_output::colored; -use tokio::time::sleep; - -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, - auth: &[VecM], - events: &[DiagnosticEvent], -) -> (); - -pub type LogResources = fn(resources: &SorobanResources) -> (); - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - InvalidAddress(#[from] stellar_strkey::DecodeError), - #[error("invalid response from server")] - InvalidResponse, - #[error("provided network passphrase {expected:?} does not match the server: {server:?}")] - InvalidNetworkPassphrase { expected: String, server: String }, - #[error("xdr processing error: {0}")] - Xdr(#[from] XdrError), - #[error("invalid rpc url: {0}")] - InvalidRpcUrl(http::uri::InvalidUri), - #[error("invalid rpc url: {0}")] - InvalidRpcUrlFromUriParts(http::uri::InvalidUriParts), - #[error("invalid friendbot url: {0}")] - InvalidUrl(String), - #[error("jsonrpc error: {0}")] - JsonRpc(#[from] jsonrpsee_core::Error), - #[error("json decoding error: {0}")] - Serde(#[from] serde_json::Error), - #[error("transaction failed: {0}")] - TransactionFailed(String), - #[error("transaction submission failed: {0}")] - TransactionSubmissionFailed(String), - #[error("expected transaction status: {0}")] - UnexpectedTransactionStatus(String), - #[error("transaction submission timeout")] - TransactionSubmissionTimeout, - #[error("transaction simulation failed: {0}")] - TransactionSimulationFailed(String), - #[error("{0} not found: {1}")] - NotFound(String, String), - #[error("Missing result in successful response")] - MissingResult, - #[error("Failed to read Error response from server")] - MissingError, - #[error("Missing signing key for account {address}")] - MissingSignerForAddress { address: String }, - #[error("cursor is not valid")] - InvalidCursor, - #[error("unexpected ({length}) simulate transaction result length")] - UnexpectedSimulateTransactionResultSize { length: usize }, - #[error("unexpected ({count}) number of operations")] - UnexpectedOperationCount { count: usize }, - #[error("Transaction contains unsupported operation type")] - UnsupportedOperationType, - #[error("unexpected contract code data type: {0:?}")] - UnexpectedContractCodeDataType(LedgerEntryData), - #[error(transparent)] - CouldNotParseContractSpec(#[from] contract::Error), - #[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")] - CannotAuthorizeRawTransaction, - - #[error("Missing result for tnx")] - MissingOp, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct SendTransactionResponse { - pub hash: String, - pub status: String, - #[serde( - rename = "errorResultXdr", - skip_serializing_if = "Option::is_none", - default - )] - pub error_result_xdr: Option, - #[serde(rename = "latestLedger")] - pub latest_ledger: u32, - #[serde( - rename = "latestLedgerCloseTime", - deserialize_with = "deserialize_number_from_string" - )] - pub latest_ledger_close_time: u32, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GetTransactionResponseRaw { - pub status: String, - #[serde( - rename = "envelopeXdr", - skip_serializing_if = "Option::is_none", - default - )] - pub envelope_xdr: Option, - #[serde(rename = "resultXdr", skip_serializing_if = "Option::is_none", default)] - pub result_xdr: Option, - #[serde( - rename = "resultMetaXdr", - skip_serializing_if = "Option::is_none", - default - )] - pub result_meta_xdr: Option, - // TODO: add ledger info and application order -} - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GetTransactionResponse { - pub status: String, - pub envelope: Option, - pub result: Option, - pub result_meta: Option, -} - -impl TryInto for GetTransactionResponseRaw { - type Error = xdr::Error; - - fn try_into(self) -> Result { - Ok(GetTransactionResponse { - status: self.status, - envelope: self - .envelope_xdr - .map(|v| ReadXdr::from_xdr_base64(v, Limits::none())) - .transpose()?, - result: self - .result_xdr - .map(|v| ReadXdr::from_xdr_base64(v, Limits::none())) - .transpose()?, - result_meta: self - .result_meta_xdr - .map(|v| ReadXdr::from_xdr_base64(v, Limits::none())) - .transpose()?, - }) - } -} - -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, - pub xdr: String, - #[serde(rename = "lastModifiedLedgerSeq")] - pub last_modified_ledger: u32, - #[serde( - rename = "liveUntilLedgerSeq", - skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_option_number_from_string", - default - )] - pub live_until_ledger_seq_ledger_seq: Option, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GetLedgerEntriesResponse { - pub entries: Option>, - #[serde(rename = "latestLedger")] - pub latest_ledger: i64, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GetNetworkResponse { - #[serde( - rename = "friendbotUrl", - skip_serializing_if = "Option::is_none", - default - )] - pub friendbot_url: Option, - pub passphrase: String, - #[serde(rename = "protocolVersion")] - pub protocol_version: u32, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GetLatestLedgerResponse { - pub id: String, - #[serde(rename = "protocolVersion")] - pub protocol_version: u32, - pub sequence: u32, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] -pub struct Cost { - #[serde( - rename = "cpuInsns", - deserialize_with = "deserialize_number_from_string" - )] - pub cpu_insns: u64, - #[serde( - rename = "memBytes", - deserialize_with = "deserialize_number_from_string" - )] - pub mem_bytes: u64, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct SimulateHostFunctionResultRaw { - #[serde(deserialize_with = "deserialize_default_from_null")] - pub auth: Vec, - pub xdr: String, -} - -#[derive(Debug)] -pub struct SimulateHostFunctionResult { - pub auth: Vec, - pub xdr: xdr::ScVal, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] -pub struct SimulateTransactionResponse { - #[serde( - rename = "minResourceFee", - deserialize_with = "deserialize_number_from_string", - default - )] - pub min_resource_fee: u64, - #[serde(default)] - pub cost: Cost, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub results: Vec, - #[serde(rename = "transactionData", default)] - pub transaction_data: String, - #[serde( - deserialize_with = "deserialize_default_from_null", - skip_serializing_if = "Vec::is_empty", - default - )] - pub events: Vec, - #[serde( - rename = "restorePreamble", - skip_serializing_if = "Option::is_none", - default - )] - pub restore_preamble: Option, - #[serde(rename = "latestLedger")] - pub latest_ledger: u32, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub error: Option, -} - -impl SimulateTransactionResponse { - /// - /// # Errors - pub fn results(&self) -> Result, Error> { - self.results - .iter() - .map(|r| { - Ok(SimulateHostFunctionResult { - auth: r - .auth - .iter() - .map(|a| { - Ok(SorobanAuthorizationEntry::from_xdr_base64( - a, - Limits::none(), - )?) - }) - .collect::>()?, - xdr: xdr::ScVal::from_xdr_base64(&r.xdr, Limits::none())?, - }) - }) - .collect() - } - - /// - /// # Errors - pub fn events(&self) -> Result, Error> { - self.events - .iter() - .map(|e| Ok(DiagnosticEvent::from_xdr_base64(e, Limits::none())?)) - .collect() - } - - /// - /// # Errors - pub fn transaction_data(&self) -> Result { - Ok(SorobanTransactionData::from_xdr_base64( - &self.transaction_data, - Limits::none(), - )?) - } -} - -#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] -pub struct RestorePreamble { - #[serde(rename = "transactionData")] - pub transaction_data: String, - #[serde( - rename = "minResourceFee", - deserialize_with = "deserialize_number_from_string" - )] - pub min_resource_fee: u64, -} - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GetEventsResponse { - #[serde(deserialize_with = "deserialize_default_from_null")] - pub events: Vec, - #[serde(rename = "latestLedger")] - pub latest_ledger: u32, -} - -// Determines whether or not a particular filter matches a topic based on the -// same semantics as the RPC server: -// -// - for an exact segment match, the filter is a base64-encoded ScVal -// - for a wildcard, single-segment match, the string "*" matches exactly one -// segment -// -// The expectation is that a `filter` is a comma-separated list of segments that -// has previously been validated, and `topic` is the list of segments applicable -// for this event. -// -// [API -// 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 - .iter() - .enumerate() - .all(|(i, s)| *s == "*" || topic[i] == *s) -} - -#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] -pub struct Event { - #[serde(rename = "type")] - pub event_type: String, - - pub ledger: u32, - #[serde(rename = "ledgerClosedAt")] - pub ledger_closed_at: String, - - pub id: String, - #[serde(rename = "pagingToken")] - pub paging_token: String, - - #[serde(rename = "contractId")] - pub contract_id: String, - pub topic: Vec, - pub value: String, -} - -impl Display for Event { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!( - f, - "Event {} [{}]:", - self.paging_token, - self.event_type.to_ascii_uppercase() - )?; - writeln!( - f, - " Ledger: {} (closed at {})", - self.ledger, self.ledger_closed_at - )?; - writeln!(f, " Contract: {}", self.contract_id)?; - writeln!(f, " Topics:")?; - for topic in &self.topic { - let scval = - xdr::ScVal::from_xdr_base64(topic, Limits::none()).map_err(|_| std::fmt::Error)?; - writeln!(f, " {scval:?}")?; - } - let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none()) - .map_err(|_| std::fmt::Error)?; - writeln!(f, " Value: {scval:?}") - } -} - -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() { - println!("{self}"); - return Ok(()); - } - - let color = match self.event_type.as_str() { - "system" => Color::Yellow, - _ => Color::Blue, - }; - colored!( - stdout, - "{}Event{} {}{}{} [{}{}{}{}]:\n", - bold!(true), - bold!(false), - fg!(Some(Color::Green)), - self.paging_token, - reset!(), - bold!(true), - fg!(Some(color)), - self.event_type.to_ascii_uppercase(), - reset!(), - )?; - - colored!( - stdout, - " Ledger: {}{}{} (closed at {}{}{})\n", - fg!(Some(Color::Green)), - self.ledger, - reset!(), - fg!(Some(Color::Green)), - self.ledger_closed_at, - reset!(), - )?; - - colored!( - stdout, - " Contract: {}{}{}\n", - fg!(Some(Color::Green)), - self.contract_id, - reset!(), - )?; - - colored!(stdout, " Topics:\n")?; - for topic in &self.topic { - let scval = xdr::ScVal::from_xdr_base64(topic, Limits::none())?; - colored!( - stdout, - " {}{:?}{}\n", - fg!(Some(Color::Green)), - scval, - reset!(), - )?; - } - - let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())?; - colored!( - stdout, - " Value: {}{:?}{}\n", - fg!(Some(Color::Green)), - scval, - reset!(), - )?; - - Ok(()) - } -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] -pub enum EventType { - All, - Contract, - System, -} - -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub enum EventStart { - Ledger(u32), - Cursor(String), -} - -#[derive(Debug)] -pub struct FullLedgerEntry { - pub key: LedgerKey, - pub val: LedgerEntryData, - pub last_modified_ledger: u32, - pub live_until_ledger_seq: u32, -} - -#[derive(Debug)] -pub struct FullLedgerEntries { - pub entries: Vec, - pub latest_ledger: i64, -} - -pub struct Client { - base_url: String, -} - -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. - // Jsonrpsee requires a port to always be present even if one can be - // inferred. This may change: https://github.com/paritytech/jsonrpsee/issues/1048. - let uri = base_url.parse::().map_err(Error::InvalidRpcUrl)?; - let mut parts = uri.into_parts(); - if let (Some(scheme), Some(authority)) = (&parts.scheme, &parts.authority) { - if authority.port().is_none() { - let port = match scheme.as_str() { - "http" => Some(80), - "https" => Some(443), - _ => None, - }; - if let Some(port) = port { - let host = authority.host(); - parts.authority = Some( - Authority::from_str(&format!("{host}:{port}")) - .map_err(Error::InvalidRpcUrl)?, - ); - } - } - } - let uri = Uri::from_parts(parts).map_err(Error::InvalidRpcUrlFromUriParts)?; - tracing::trace!(?uri); - Ok(Self { - base_url: uri.to_string(), - }) - } - - /// - /// # 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)?) - } - - /// - /// # Errors - pub async fn friendbot_url(&self) -> Result { - let network = self.get_network().await?; - tracing::trace!("{network:#?}"); - network.friendbot_url.ok_or_else(|| { - Error::NotFound( - "Friendbot".to_string(), - "Friendbot is not available on this network".to_string(), - ) - }) - } - /// - /// # Errors - pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result { - let server = self.get_network().await?.passphrase; - if let Some(expected) = expected { - if expected != server { - return Err(Error::InvalidNetworkPassphrase { - expected: expected.to_string(), - server, - }); - } - } - 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 - .client()? - .request("getLatestLedger", rpc_params![]) - .await?) - } - - /// - /// # Errors - pub async fn get_account(&self, address: &str) -> Result { - tracing::trace!("Getting address {}", address); - let key = LedgerKey::Account(LedgerKeyAccount { - account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( - stellar_strkey::ed25519::PublicKey::from_string(address)?.0, - ))), - }); - let keys = Vec::from([key]); - 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 "# - ), - )); - } - let ledger_entry = &entries[0]; - let mut read = Limited::new(ledger_entry.xdr.as_bytes(), Limits::none()); - if let LedgerEntryData::Account(entry) = LedgerEntryData::read_xdr_base64(&mut read)? { - tracing::trace!(account=?entry); - Ok(entry) - } else { - Err(Error::InvalidResponse) - } - } - - /// - /// # Errors - pub async fn send_transaction( - &self, - tx: &TransactionEnvelope, - ) -> Result { - let client = self.client()?; - tracing::trace!("Sending:\n{tx:#?}"); - let SendTransactionResponse { - hash, - error_result_xdr, - status, - .. - } = client - .request( - "sendTransaction", - rpc_params![tx.to_xdr_base64(Limits::none())?], - ) - .await - .map_err(|err| { - Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}")) - })?; - - if status == "ERROR" { - let error = error_result_xdr - .ok_or(Error::MissingError) - .and_then(|x| { - TransactionResult::read_xdr_base64(&mut Limited::new( - x.as_bytes(), - Limits::none(), - )) - .map_err(|_| Error::InvalidResponse) - }) - .map(|r| r.result); - tracing::error!("TXN 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(); - // TODO: parameterize the timeout instead of using a magic constant - if duration.as_secs() > 10 { - return Err(Error::TransactionSubmissionTimeout); - } - sleep(Duration::from_secs(1)).await; - } - } - - /// - /// # Errors - pub async fn simulate_transaction( - &self, - tx: &TransactionEnvelope, - ) -> Result { - tracing::trace!("Simulating:\n{tx:#?}"); - let base64_tx = tx.to_xdr_base64(Limits::none())?; - let mut builder = ObjectParams::new(); - builder.insert("transaction", base64_tx)?; - let response: SimulateTransactionResponse = self - .client()? - .request("simulateTransaction", builder) - .await?; - tracing::trace!("Simulation response:\n {response:#?}"); - match response.error { - None => Ok(response), - Some(e) => { - log::diagnostic_events(&response.events, tracing::Level::ERROR); - Err(Error::TransactionSimulationFailed(e)) - } - } - } - - /// - /// # Errors - pub async fn send_assembled_transaction( - &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) - .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()? - .request("getTransaction", rpc_params![tx_id]) - .await?) - } - - /// - /// # Errors - pub async fn get_ledger_entries( - &self, - keys: &[LedgerKey], - ) -> Result { - let mut base64_keys: Vec = vec![]; - for k in keys { - let base64_result = k.to_xdr_base64(Limits::none()); - if base64_result.is_err() { - return Err(Error::Xdr(XdrError::Invalid)); - } - base64_keys.push(k.to_xdr_base64(Limits::none())?); - } - Ok(self - .client()? - .request("getLedgerEntries", rpc_params![base64_keys]) - .await?) - } - - /// - /// # Errors - pub async fn get_full_ledger_entries( - &self, - ledger_keys: &[LedgerKey], - ) -> Result { - let keys = ledger_keys - .iter() - .filter(|key| !matches!(key, LedgerKey::Ttl(_))) - .map(Clone::clone) - .collect::>(); - tracing::trace!("keys: {keys:#?}"); - let GetLedgerEntriesResponse { - entries, - latest_ledger, - } = self.get_ledger_entries(&keys).await?; - tracing::trace!("raw: {entries:#?}"); - let entries = entries - .unwrap_or_default() - .iter() - .map( - |LedgerEntryResult { - key, - xdr, - last_modified_ledger, - live_until_ledger_seq_ledger_seq, - }| { - Ok(FullLedgerEntry { - key: LedgerKey::from_xdr_base64(key, Limits::none())?, - val: LedgerEntryData::from_xdr_base64(xdr, Limits::none())?, - live_until_ledger_seq: live_until_ledger_seq_ledger_seq.unwrap_or_default(), - last_modified_ledger: *last_modified_ledger, - }) - }, - ) - .collect::, Error>>()?; - tracing::trace!("parsed: {entries:#?}"); - Ok(FullLedgerEntries { - entries, - latest_ledger, - }) - } - /// - /// # Errors - pub async fn get_events( - &self, - start: EventStart, - event_type: Option, - contract_ids: &[String], - topics: &[String], - limit: Option, - ) -> Result { - let mut filters = serde_json::Map::new(); - - event_type - .and_then(|t| match t { - EventType::All => None, // all is the default, so avoid incl. the param - EventType::Contract => Some("contract"), - EventType::System => Some("system"), - }) - .map(|t| filters.insert("type".to_string(), t.into())); - - filters.insert("topics".to_string(), topics.into()); - filters.insert("contractIds".to_string(), contract_ids.into()); - - let mut pagination = serde_json::Map::new(); - if let Some(limit) = limit { - pagination.insert("limit".to_string(), limit.into()); - } - - let mut oparams = ObjectParams::new(); - match start { - EventStart::Ledger(l) => oparams.insert("startLedger", l)?, - EventStart::Cursor(c) => { - pagination.insert("cursor".to_string(), c.into()); - } - }; - oparams.insert("filters", vec![filters])?; - oparams.insert("pagination", pagination)?; - - Ok(self.client()?.request("getEvents", oparams).await?) - } - - /// - /// # Errors - pub async fn get_contract_data( - &self, - contract_id: &[u8; 32], - ) -> Result { - // Get the contract from the network - let contract_key = LedgerKey::ContractData(xdr::LedgerKeyContractData { - contract: xdr::ScAddress::Contract(xdr::Hash(*contract_id)), - key: xdr::ScVal::LedgerKeyContractInstance, - durability: xdr::ContractDataDurability::Persistent, - }); - let contract_ref = self.get_ledger_entries(&[contract_key]).await?; - let entries = contract_ref.entries.unwrap_or_default(); - if entries.is_empty() { - let contract_address = stellar_strkey::Contract(*contract_id).to_string(); - return Err(Error::NotFound("Contract".to_string(), contract_address)); - } - let contract_ref_entry = &entries[0]; - match LedgerEntryData::from_xdr_base64(&contract_ref_entry.xdr, Limits::none())? { - LedgerEntryData::ContractData(contract_data) => Ok(contract_data), - scval => Err(Error::UnexpectedContractCodeDataType(scval)), - } - } - - /// - /// # Errors - pub async fn get_remote_wasm(&self, contract_id: &[u8; 32]) -> Result, Error> { - match self.get_contract_data(contract_id).await? { - xdr::ContractDataEntry { - val: - xdr::ScVal::ContractInstance(xdr::ScContractInstance { - executable: xdr::ContractExecutable::Wasm(hash), - .. - }), - .. - } => self.get_remote_wasm_from_hash(hash).await, - scval => Err(Error::UnexpectedToken(scval)), - } - } - - /// - /// # 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?; - let entries = contract_data.entries.unwrap_or_default(); - if entries.is_empty() { - return Err(Error::NotFound( - "Contract Code".to_string(), - hex::encode(hash), - )); - } - let contract_data_entry = &entries[0]; - match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? { - LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()), - scval => Err(Error::UnexpectedContractCodeDataType(scval)), - } - } - /// - /// # Errors - pub async fn get_remote_contract_spec( - &self, - contract_id: &[u8; 32], - ) -> Result, Error> { - 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( - 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, - .. - }) => Ok(soroban_spec::read::parse_raw( - &token::StellarAssetSpec::spec_xdr(), - )?), - _ => Err(Error::Xdr(XdrError::Invalid)), - } - } -} - -fn extract_events(tx_meta: &TransactionMeta) -> Vec { - match tx_meta { - TransactionMeta::V3(TransactionMetaV3 { - soroban_meta: Some(meta), - .. - }) => { - // NOTE: we assume there can only be one operation, since we only send one - if meta.diagnostic_events.len() == 1 { - meta.diagnostic_events.clone().into() - } else if meta.events.len() == 1 { - meta.events - .iter() - .map(|e| DiagnosticEvent { - in_successful_contract_call: true, - event: e.clone(), - }) - .collect() - } else { - Vec::new() - } - } - _ => Vec::new(), - } -} - -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)?; - Ok((toid_part, start_index)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn simulation_transaction_response_parsing() { - let s = r#"{ - "minResourceFee": "100000000", - "cost": { "cpuInsns": "1000", "memBytes": "1000" }, - "transactionData": "", - "latestLedger": 1234 - }"#; - - let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap(); - assert_eq!(resp.min_resource_fee, 100_000_000); - } - - #[test] - fn simulation_transaction_response_parsing_mostly_empty() { - let s = r#"{ - "latestLedger": 1234 - }"#; - - let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap(); - assert_eq!(resp.latest_ledger, 1_234); - } - - #[test] - 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/"); - let client = Client::new("https://example.com").unwrap(); - 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/"); - let client = Client::new("https://example.com:8080").unwrap(); - 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"); - let client = Client::new("https://example.com/a/b/c").unwrap(); - 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/"); - let client = Client::new("https://example.com/a/b/c/").unwrap(); - 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/"); - 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/"); - } - - #[test] - // Taken from [RPC server - // tests](https://github.com/stellar/soroban-tools/blob/main/cmd/soroban-rpc/internal/methods/get_events_test.go#L21). - fn test_does_topic_match() { - struct TestCase<'a> { - name: &'a str, - filter: Vec<&'a str>, - includes: Vec>, - excludes: Vec>, - } - - let xfer = "AAAABQAAAAh0cmFuc2Zlcg=="; - let number = "AAAAAQB6Mcc="; - let star = "*"; - - for tc in vec![ - // No filter means match nothing. - TestCase { - name: "", - filter: vec![], - includes: vec![], - excludes: vec![vec![xfer]], - }, - // "*" should match "transfer/" but not "transfer/transfer" or - // "transfer/amount", because * is specified as a SINGLE segment - // wildcard. - TestCase { - name: "*", - filter: vec![star], - includes: vec![vec![xfer]], - excludes: vec![vec![xfer, xfer], vec![xfer, number]], - }, - // "*/transfer" should match anything preceding "transfer", but - // nothing that isn't exactly two segments long. - TestCase { - name: "*/transfer", - filter: vec![star, xfer], - includes: vec![vec![number, xfer], vec![xfer, xfer]], - excludes: vec![ - vec![number], - vec![number, number], - vec![number, xfer, number], - vec![xfer], - vec![xfer, number], - vec![xfer, xfer, xfer], - ], - }, - // The inverse case of before: "transfer/*" should match any single - // segment after a segment that is exactly "transfer", but no - // additional segments. - TestCase { - name: "transfer/*", - filter: vec![xfer, star], - includes: vec![vec![xfer, number], vec![xfer, xfer]], - excludes: vec![ - vec![number], - vec![number, number], - vec![number, xfer, number], - vec![xfer], - vec![number, xfer], - vec![xfer, xfer, xfer], - ], - }, - // Here, we extend to exactly two wild segments after transfer. - TestCase { - name: "transfer/*/*", - filter: vec![xfer, star, star], - includes: vec![vec![xfer, number, number], vec![xfer, xfer, xfer]], - excludes: vec![ - vec![number], - vec![number, number], - vec![number, xfer], - vec![number, xfer, number, number], - vec![xfer], - vec![xfer, xfer, xfer, xfer], - ], - }, - // Here, we ensure wildcards can be in the middle of a filter: only - // exact matches happen on the ends, while the middle can be - // anything. - TestCase { - name: "transfer/*/number", - filter: vec![xfer, star, number], - includes: vec![vec![xfer, number, number], vec![xfer, xfer, number]], - excludes: vec![ - vec![number], - vec![number, number], - vec![number, number, number], - vec![number, xfer, number], - vec![xfer], - vec![number, xfer], - vec![xfer, xfer, xfer], - vec![xfer, number, xfer], - ], - }, - ] { - for topic in tc.includes { - assert!( - does_topic_match( - &topic - .iter() - .map(std::string::ToString::to_string) - .collect::>(), - &tc.filter - .iter() - .map(std::string::ToString::to_string) - .collect::>() - ), - "test: {}, topic ({:?}) should be matched by filter ({:?})", - tc.name, - topic, - tc.filter - ); - } - - for topic in tc.excludes { - assert!( - !does_topic_match( - // make deep copies of the vecs - &topic - .iter() - .map(std::string::ToString::to_string) - .collect::>(), - &tc.filter - .iter() - .map(std::string::ToString::to_string) - .collect::>() - ), - "test: {}, topic ({:?}) should NOT be matched by filter ({:?})", - tc.name, - topic, - tc.filter - ); - } - } - } -} diff --git a/cmd/crates/soroban-rpc/src/log.rs b/cmd/crates/soroban-rpc/src/log.rs deleted file mode 100644 index 361268148..000000000 --- a/cmd/crates/soroban-rpc/src/log.rs +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 68af67a4e..000000000 --- a/cmd/crates/soroban-rpc/src/log/diagnostic_events.rs +++ /dev/null @@ -1,11 +0,0 @@ -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/crates/soroban-rpc/src/txn.rs b/cmd/crates/soroban-rpc/src/txn.rs deleted file mode 100644 index 35b0c7192..000000000 --- a/cmd/crates/soroban-rpc/src/txn.rs +++ /dev/null @@ -1,724 +0,0 @@ -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 super::{Client, Error, RestorePreamble, SimulateTransactionResponse}; - -use super::{LogEvents, LogResources}; - -pub struct Assembled { - txn: Transaction, - 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()), - tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(self.txn.clone()), - }; - 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. - /// - /// # 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 { - 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()) - } else { - Ok(self) - } - } - - /// Returns a reference to the original transaction. - #[must_use] - pub fn transaction(&self) -> &Transaction { - &self.txn - } - - /// 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, - 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; - self - } - - /// - /// # Errors - #[must_use] - pub fn auth_entries(&self) -> VecM { - self.txn - .operations - .first() - .and_then(|op| match op.body { - OperationBody::InvokeHostFunction(ref body) => (matches!( - body.auth.first().map(|x| &x.root_invocation.function), - Some(&SorobanAuthorizedFunction::ContractFn(_)) - )) - .then_some(body.auth.clone()), - _ => None, - }) - .unwrap_or_default() - } - - /// - /// # Errors - pub fn log( - &self, - log_events: Option, - log_resources: Option, - ) -> Result<(), Error> { - if let TransactionExt::V1(SorobanTransactionData { - resources: resources @ SorobanResources { footprint, .. }, - .. - }) = &self.txn.ext - { - if let Some(log) = log_resources { - log(resources); - } - if let Some(log) = log_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, -) -> Result { - let mut tx = raw.clone(); - - // Right now simulate.results is one-result-per-function, and assumes there is only one - // operation in the txn, so we need to enforce that here. I (Paul) think that is a bug - // in soroban-rpc.simulateTransaction design, and we should fix it there. - // TODO: We should to better handling so non-soroban txns can be a passthrough here. - if tx.operations.len() != 1 { - return Err(Error::UnexpectedOperationCount { - count: tx.operations.len(), - }); - } - - let transaction_data = simulation.transaction_data()?; - - let mut op = tx.operations[0].clone(); - if let OperationBody::InvokeHostFunction(ref mut body) = &mut op.body { - if body.auth.is_empty() { - if simulation.results.len() != 1 { - return Err(Error::UnexpectedSimulateTransactionResultSize { - length: simulation.results.len(), - }); - } - - let auths = simulation - .results - .iter() - .map(|r| { - VecM::try_from( - r.auth - .iter() - .map(|v| SorobanAuthorizationEntry::from_xdr_base64(v, Limits::none())) - .collect::, _>>()?, - ) - }) - .collect::, _>>()?; - if !auths.is_empty() { - body.auth = auths[0].clone(); - } - } - } - - // update the fees of the actual transaction to meet the minimum resource fees. - 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 - + u32::try_from(simulation.min_resource_fee) - .map_err(|_| Error::LargeFee(simulation.min_resource_fee))?, - ) * 115) - / 100; - - tx.operations = vec![op].try_into()?; - tx.ext = TransactionExt::V1(transaction_data); - 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( - 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 { - let transaction_data = - SorobanTransactionData::from_xdr_base64(&restore.transaction_data, Limits::none())?; - let fee = u32::try_from(restore.min_resource_fee) - .map_err(|_| Error::LargeFee(restore.min_resource_fee))?; - Ok(Transaction { - source_account: parent.source_account.clone(), - fee: parent - .fee - .checked_add(fee) - .ok_or(Error::LargeFee(restore.min_resource_fee))?, - seq_num: parent.seq_num.clone(), - cond: Preconditions::None, - memo: Memo::None, - operations: vec![Operation { - source_account: None, - body: OperationBody::RestoreFootprint(RestoreFootprintOp { - ext: ExtensionPoint::V0, - }), - }] - .try_into()?, - ext: TransactionExt::V1(transaction_data), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - use super::super::SimulateHostFunctionResultRaw; - use soroban_env_host::xdr::{ - self, 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"; - - fn transaction_data() -> SorobanTransactionData { - SorobanTransactionData { - resources: SorobanResources { - footprint: LedgerFootprint { - read_only: VecM::default(), - read_write: VecM::default(), - }, - instructions: 0, - read_bytes: 5, - write_bytes: 0, - }, - resource_fee: 0, - ext: ExtensionPoint::V0, - } - } - - fn simulation_response() -> SimulateTransactionResponse { - let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0; - let fn_auth = &SorobanAuthorizationEntry { - credentials: xdr::SorobanCredentials::Address(xdr::SorobanAddressCredentials { - address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( - source_bytes, - )))), - nonce: 0, - signature_expiration_ledger: 0, - signature: ScVal::Void, - }), - root_invocation: SorobanAuthorizedInvocation { - function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { - contract_address: ScAddress::Contract(Hash([0; 32])), - function_name: ScSymbol("fn".try_into().unwrap()), - args: VecM::default(), - }), - sub_invocations: VecM::default(), - }, - }; - - SimulateTransactionResponse { - min_resource_fee: 115, - latest_ledger: 3, - results: vec![SimulateHostFunctionResultRaw { - auth: vec![fn_auth.to_xdr_base64(Limits::none()).unwrap()], - xdr: ScVal::U32(0).to_xdr_base64(Limits::none()).unwrap(), - }], - transaction_data: transaction_data().to_xdr_base64(Limits::none()).unwrap(), - ..Default::default() - } - } - - fn single_contract_fn_transaction() -> Transaction { - let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0; - Transaction { - source_account: MuxedAccount::Ed25519(Uint256(source_bytes)), - fee: 100, - seq_num: SequenceNumber(0), - cond: Preconditions::None, - memo: Memo::None, - operations: vec![Operation { - source_account: None, - body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { - host_function: HostFunction::InvokeContract(InvokeContractArgs { - contract_address: ScAddress::Contract(Hash([0x0; 32])), - function_name: ScSymbol::default(), - args: VecM::default(), - }), - auth: VecM::default(), - }), - }] - .try_into() - .unwrap(), - ext: TransactionExt::V0, - } - } - - #[test] - fn test_assemble_transaction_updates_tx_data_from_simulation_response() { - let sim = simulation_response(); - let txn = single_contract_fn_transaction(); - let Ok(result) = assemble(&txn, &sim) else { - panic!("assemble failed"); - }; - - // validate it auto updated the tx fees from sim response fees - // since it was greater than tx.fee - assert_eq!(247, result.fee); - - // validate it updated sorobantransactiondata block in the tx ext - assert_eq!(TransactionExt::V1(transaction_data()), result.ext); - } - - #[test] - fn test_assemble_transaction_adds_the_auth_to_the_host_function() { - let sim = simulation_response(); - let txn = single_contract_fn_transaction(); - let Ok(result) = assemble(&txn, &sim) else { - panic!("assemble failed"); - }; - - assert_eq!(1, result.operations.len()); - let OperationBody::InvokeHostFunction(ref op) = result.operations[0].body else { - panic!("unexpected operation type: {:#?}", result.operations[0]); - }; - - assert_eq!(1, op.auth.len()); - let auth = &op.auth[0]; - - let xdr::SorobanAuthorizedFunction::ContractFn(xdr::InvokeContractArgs { - ref function_name, - .. - }) = auth.root_invocation.function - else { - panic!("unexpected function type"); - }; - assert_eq!("fn".to_string(), format!("{}", function_name.0)); - - let xdr::SorobanCredentials::Address(xdr::SorobanAddressCredentials { - address: - xdr::ScAddress::Account(xdr::AccountId(xdr::PublicKey::PublicKeyTypeEd25519(address))), - .. - }) = &auth.credentials - else { - panic!("unexpected credentials type"); - }; - assert_eq!( - SOURCE.to_string(), - stellar_strkey::ed25519::PublicKey(address.0).to_string() - ); - } - - #[test] - fn test_assemble_transaction_errors_for_non_invokehostfn_ops() { - let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0; - let txn = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(source_bytes)), - fee: 100, - seq_num: SequenceNumber(0), - cond: Preconditions::None, - memo: Memo::None, - operations: vec![Operation { - source_account: None, - body: OperationBody::ChangeTrust(ChangeTrustOp { - line: ChangeTrustAsset::Native, - limit: 0, - }), - }] - .try_into() - .unwrap(), - ext: TransactionExt::V0, - }; - - let result = assemble( - &txn, - &SimulateTransactionResponse { - min_resource_fee: 115, - transaction_data: transaction_data().to_xdr_base64(Limits::none()).unwrap(), - latest_ledger: 3, - ..Default::default() - }, - ); - - match result { - Ok(_) => {} - Err(e) => panic!("expected assembled operation, got: {e:#?}"), - } - } - - #[test] - fn test_assemble_transaction_errors_for_errors_for_mismatched_simulation() { - let txn = single_contract_fn_transaction(); - - let result = assemble( - &txn, - &SimulateTransactionResponse { - min_resource_fee: 115, - transaction_data: transaction_data().to_xdr_base64(Limits::none()).unwrap(), - latest_ledger: 3, - ..Default::default() - }, - ); - - match result { - Err(Error::UnexpectedSimulateTransactionResultSize { length }) => { - assert_eq!(0, length); - } - r => panic!("expected UnexpectedSimulateTransactionResultSize error, got: {r:#?}"), - } - } -}