use crate::{RpcError, SorobanRpcClient};
use serde::{Deserialize, Deserializer, Serialize};
fn deserialize_optional_string_or_int<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
Ok(match value {
Some(serde_json::Value::String(s)) => Some(s),
Some(serde_json::Value::Number(n)) => Some(n.to_string()),
Some(_) => return Err(Error::custom("expected string or number")),
None => None,
})
}
#[derive(Debug, Serialize)]
pub struct SimulateTransactionRequest {
pub transaction: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SimulateOperationResult {
#[serde(default)]
pub auth: Vec<String>,
#[serde(default)]
pub xdr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SimulateCost {
pub cpu_insns: String,
pub mem_bytes: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SimulateRestorePreamble {
pub min_resource_fee: String,
pub transaction_data: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SimulateResponse {
#[serde(default)]
pub transaction_data: String,
#[serde(default)]
pub min_resource_fee: String,
#[serde(default)]
pub restore_preamble: Option<SimulateRestorePreamble>,
#[serde(default)]
pub results: Vec<SimulateOperationResult>,
#[serde(default)]
pub cost: Option<SimulateCost>,
#[serde(default, deserialize_with = "deserialize_optional_string_or_int")]
pub latest_ledger: Option<String>,
#[serde(default)]
pub events: Vec<serde_json::Value>,
#[serde(default)]
pub state_changes: Vec<serde_json::Value>,
#[serde(default)]
pub error: Option<String>,
}
pub async fn simulate_transaction(
client: &SorobanRpcClient,
envelope: &str,
) -> Result<SimulateResponse, RpcError> {
if envelope.trim().is_empty() {
return Err(RpcError::Rpc("Transaction envelope is empty".to_string()));
}
let request = SimulateTransactionRequest {
transaction: envelope.to_string(),
};
let response: SimulateResponse = client.request("simulateTransaction", request).await?;
Ok(response)
}
pub fn validate_envelope(envelope: &str) -> Result<(), RpcError> {
if envelope.trim().is_empty() {
return Err(RpcError::Rpc("Transaction envelope is empty".to_string()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_serialization() {
let request = SimulateTransactionRequest {
transaction: "AAAAEnvelope===".to_string(),
};
let json = serde_json::to_value(request).unwrap();
assert_eq!(json["transaction"], "AAAAEnvelope===");
}
#[test]
fn test_response_deserialize_full() {
let raw = r#"{
"transactionData": "AAAAAdata",
"minResourceFee": "1000",
"restorePreamble": {
"minResourceFee": "500",
"transactionData": "AAAABdata"
},
"results": [
{"auth": ["AAAAauth1"], "xdr": "AAAAxdr1"}
],
"cost": {"cpuInsns": "5000", "memBytes": "2048"},
"latestLedger": "12345",
"events": [{"type": "diagnostic"}],
"stateChanges": [{"type": "ledgerEntry"}]
}"#;
let response: SimulateResponse = serde_json::from_str(raw).unwrap();
assert_eq!(response.transaction_data, "AAAAAdata");
assert_eq!(response.min_resource_fee, "1000");
let restore = response.restore_preamble.unwrap();
assert_eq!(restore.min_resource_fee, "500");
assert_eq!(restore.transaction_data, "AAAABdata");
assert_eq!(response.results.len(), 1);
assert_eq!(response.results[0].auth, vec!["AAAAauth1".to_string()]);
assert_eq!(response.results[0].xdr, "AAAAxdr1");
let cost = response.cost.unwrap();
assert_eq!(cost.cpu_insns, "5000");
assert_eq!(cost.mem_bytes, "2048");
assert_eq!(response.latest_ledger, Some("12345".to_string()));
assert_eq!(response.events.len(), 1);
assert_eq!(response.state_changes.len(), 1);
assert_eq!(response.error, None);
}
#[test]
fn test_response_deserialize_latest_ledger_as_integer() {
let raw = r#"{
"transactionData": "AAAAAdata",
"latestLedger": 4718305
}"#;
let response: SimulateResponse = serde_json::from_str(raw).unwrap();
assert_eq!(response.latest_ledger, Some("4718305".to_string()));
}
#[test]
fn test_response_deserialize_minimal() {
let raw = r#"{"transactionData": "AAAAAdata"}"#;
let response: SimulateResponse = serde_json::from_str(raw).unwrap();
assert_eq!(response.min_resource_fee, "");
assert!(response.restore_preamble.is_none());
assert!(response.results.is_empty());
assert!(response.cost.is_none());
assert!(response.events.is_empty());
assert!(response.state_changes.is_empty());
assert_eq!(response.error, None);
}
#[test]
fn test_response_deserialize_error_field() {
let raw = r#"{
"transactionData": "",
"error": "host function failed"
}"#;
let response: SimulateResponse = serde_json::from_str(raw).unwrap();
assert_eq!(response.error, Some("host function failed".to_string()));
}
#[test]
fn test_response_deserialize_malformed() {
let raw = r#"{"transactionData": }"#;
assert!(serde_json::from_str::<SimulateResponse>(raw).is_err());
}
#[test]
fn test_empty_envelope_rejected() {
assert!(validate_envelope(" ").is_err());
assert!(validate_envelope("").is_err());
assert!(validate_envelope("AAAAEnvelope===").is_ok());
}
}