Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/core/src/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
pub mod archive;
pub mod config;
pub mod rpc;

pub use rpc::{
SimulateCost, SimulateFootprint, SimulateResult, SimulateTransactionResponse,
SorobanRpcClient,
};
218 changes: 185 additions & 33 deletions crates/core/src/network/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,109 @@ use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};

// ── simulateTransaction response types ──────────────────────────────────────

/// Ledger footprint returned by `simulateTransaction`.
///
/// Contains the read-only and read-write ledger keys the transaction will
/// access, expressed as base64-encoded XDR `LedgerKey` values.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulateFootprint {
/// Keys the transaction reads but does not modify.
#[serde(rename = "readOnly", default)]
pub read_only: Vec<String>,
/// Keys the transaction reads and may modify.
#[serde(rename = "readWrite", default)]
pub read_write: Vec<String>,
}

/// Per-invocation authorization entry returned by `simulateTransaction`.
///
/// Each entry is a base64-encoded XDR `SorobanAuthorizationEntry` that the
/// caller must sign (or leave unsigned for `invoker_contract_auth`) before
/// submitting the transaction.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulateAuthEntry {
/// Base64-encoded XDR `SorobanAuthorizationEntry`.
pub xdr: String,
}

/// Resource cost estimates returned by `simulateTransaction`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulateCost {
/// CPU instruction count consumed.
#[serde(rename = "cpuInsns", default)]
pub cpu_insns: String,
/// Memory bytes consumed.
#[serde(rename = "memBytes", default)]
pub mem_bytes: String,
}

/// Soroban resource limits and fees returned by `simulateTransaction`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulateSorobanData {
/// Base64-encoded XDR `SorobanTransactionData` (footprint + resource limits).
pub data: String,
/// Minimum resource fee in stroops.
#[serde(rename = "minResourceFee")]
pub min_resource_fee: String,
}

/// Typed response from the `simulateTransaction` RPC method.
///
/// Callers use `soroban_data` to stamp the transaction's `SorobanTransactionData`
/// extension and `auth` to populate the authorization entries before submission.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulateTransactionResponse {
/// Latest ledger sequence number at the time of simulation.
#[serde(rename = "latestLedger")]
pub latest_ledger: u32,
/// Soroban resource data (footprint + fees) to attach to the transaction.
#[serde(rename = "transactionData", default)]
pub soroban_data: Option<String>,
/// Minimum resource fee in stroops required for submission.
#[serde(rename = "minResourceFee", default)]
pub min_resource_fee: Option<String>,
/// Authorization entries that must be signed before submission.
#[serde(default)]
pub auth: Vec<String>,
/// Return value of the simulated invocation (base64 XDR `ScVal`), if any.
#[serde(default)]
pub results: Vec<SimulateResult>,
/// Error message if the simulation failed.
#[serde(default)]
pub error: Option<String>,
/// Diagnostic events emitted during simulation.
#[serde(default)]
pub events: Vec<String>,
/// Cost estimates for the simulation.
#[serde(default)]
pub cost: Option<SimulateCost>,
}

/// A single invocation result within a `simulateTransaction` response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulateResult {
/// Base64-encoded XDR `ScVal` return value.
#[serde(default)]
pub xdr: String,
/// Authorization entries required for this invocation.
#[serde(default)]
pub auth: Vec<String>,
}

impl SimulateTransactionResponse {
/// Returns `true` if the simulation completed without an error.
pub fn is_success(&self) -> bool {
self.error.is_none()
}

/// Convenience accessor for the first return value XDR, if present.
pub fn return_value_xdr(&self) -> Option<&str> {
self.results.first().map(|r| r.xdr.as_str())
}
}

/// Primary entry point for Soroban network communication.
#[derive(Debug, Clone)]
pub struct SorobanRpcClient {
Expand Down Expand Up @@ -42,33 +145,6 @@ struct JsonRpcError {
message: String,
}

/// Transaction status in Soroban.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TransactionStatus {
Success,
NotFound,
Failed,
}

/// Response for the `getTransaction` RPC method.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTransactionResponse {
pub status: TransactionStatus,
pub latest_ledger: u32,
pub latest_ledger_close_time: Option<u64>,
pub oldest_ledger: Option<u32>,
pub oldest_ledger_close_time: Option<u64>,
pub ledger: Option<u32>,
pub created_at: Option<String>,
pub application_order: Option<u32>,
pub fee_bump: Option<String>,
pub envelope_xdr: Option<String>,
pub result_xdr: Option<String>,
pub result_meta_xdr: Option<String>,
}

impl SorobanRpcClient {
/// Create a new `SorobanRpcClient` from a [`NetworkConfig`].
pub fn new(config: &NetworkConfig) -> Self {
Expand All @@ -93,12 +169,40 @@ impl SorobanRpcClient {
self.call("getTransaction", params).await
}

/// Simulate a transaction given its XDR envelope.
pub async fn simulate_transaction(&self, tx_xdr: &str) -> PrismResult<serde_json::Value> {
let params = serde_json::json!({
"transaction": tx_xdr,
});
self.call("simulateTransaction", params).await
/// Simulate a transaction against the current ledger state.
///
/// Fires the `simulateTransaction` JSON-RPC method and returns a typed
/// [`SimulateTransactionResponse`] containing:
/// - `soroban_data` — the `SorobanTransactionData` XDR to stamp onto the
/// transaction before submission (footprint + resource limits).
/// - `min_resource_fee` — the minimum fee in stroops required.
/// - `auth` — authorization entries that must be signed by the relevant
/// parties before the transaction is submitted.
/// - `results` — per-invocation return values.
///
/// If the node returns an `error` field the method returns
/// [`PrismError::RpcError`] so callers can surface the simulation failure
/// without having to inspect the raw JSON.
///
/// # Arguments
/// * `tx_xdr` — base64-encoded XDR of the unsigned `TransactionEnvelope`.
pub async fn simulate_transaction(
&self,
tx_xdr: &str,
) -> PrismResult<SimulateTransactionResponse> {
let params = serde_json::json!({ "transaction": tx_xdr });
let response: SimulateTransactionResponse =
self.call("simulateTransaction", params).await?;

// Surface simulation-level errors as a proper Rust error so callers
// don't need to inspect the struct themselves.
if let Some(ref err) = response.error {
return Err(PrismError::RpcError(format!(
"simulateTransaction failed: {err}"
)));
}

Ok(response)
}

/// Fetch ledger entries by their XDR keys.
Expand Down Expand Up @@ -127,6 +231,7 @@ impl SorobanRpcClient {
self.call("getLatestLedger", serde_json::json!({})).await
}

/// Internal JSON-RPC call with retry and rate-limit backoff.
async fn call<T: for<'de> Deserialize<'de>>(
&self,
method: &str,
Expand Down Expand Up @@ -262,4 +367,51 @@ mod tests {
assert_eq!(got, expected);
}
}
#[test]
fn test_simulate_response_is_success() {
let ok = SimulateTransactionResponse {
latest_ledger: 100,
soroban_data: Some("AAAA".to_string()),
min_resource_fee: Some("1000".to_string()),
auth: vec![],
results: vec![],
error: None,
events: vec![],
cost: None,
};
assert!(ok.is_success());

let err = SimulateTransactionResponse {
error: Some("contract trap".to_string()),
..ok
};
assert!(!err.is_success());
}

#[test]
fn test_simulate_response_deserialization() {
let json = r#"{
"jsonrpc": "2.0",
"id": 1,
"result": {
"latestLedger": 200,
"transactionData": "AAAAXDR=",
"minResourceFee": "5000",
"auth": ["AUTHXDR="],
"results": [{"xdr": "RETVAL=", "auth": []}],
"events": []
}
}"#;

let resp: JsonRpcResponse<SimulateTransactionResponse> =
serde_json::from_str(json).unwrap();
let result = resp.result.unwrap();

assert_eq!(result.latest_ledger, 200);
assert_eq!(result.soroban_data.as_deref(), Some("AAAAXDR="));
assert_eq!(result.min_resource_fee.as_deref(), Some("5000"));
assert_eq!(result.auth, vec!["AUTHXDR="]);
assert_eq!(result.return_value_xdr(), Some("RETVAL="));
assert!(result.is_success());
}
}