1use crate::{RpcError, SorobanRpcClient};
7use serde::{Deserialize, Deserializer, Serialize};
8
9fn deserialize_optional_string_or_int<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
11where
12 D: Deserializer<'de>,
13{
14 use serde::de::Error;
15 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
16 Ok(match value {
17 Some(serde_json::Value::String(s)) => Some(s),
18 Some(serde_json::Value::Number(n)) => Some(n.to_string()),
19 Some(_) => return Err(Error::custom("expected string or number")),
20 None => None,
21 })
22}
23
24#[derive(Debug, Serialize)]
26pub struct SimulateTransactionRequest {
27 pub transaction: String,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32#[serde(rename_all = "camelCase")]
33pub struct SimulateOperationResult {
34 #[serde(default)]
36 pub auth: Vec<String>,
37 #[serde(default)]
39 pub xdr: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44#[serde(rename_all = "camelCase")]
45pub struct SimulateCost {
46 pub cpu_insns: String,
47 pub mem_bytes: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
52#[serde(rename_all = "camelCase")]
53pub struct SimulateRestorePreamble {
54 pub min_resource_fee: String,
55 pub transaction_data: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
60#[serde(rename_all = "camelCase")]
61pub struct SimulateResponse {
62 #[serde(default)]
64 pub transaction_data: String,
65 #[serde(default)]
67 pub min_resource_fee: String,
68 #[serde(default)]
70 pub restore_preamble: Option<SimulateRestorePreamble>,
71 #[serde(default)]
73 pub results: Vec<SimulateOperationResult>,
74 #[serde(default)]
76 pub cost: Option<SimulateCost>,
77 #[serde(default, deserialize_with = "deserialize_optional_string_or_int")]
80 pub latest_ledger: Option<String>,
81 #[serde(default)]
83 pub events: Vec<serde_json::Value>,
84 #[serde(default)]
86 pub state_changes: Vec<serde_json::Value>,
87 #[serde(default)]
90 pub error: Option<String>,
91}
92
93pub async fn simulate_transaction(
98 client: &SorobanRpcClient,
99 envelope: &str,
100) -> Result<SimulateResponse, RpcError> {
101 if envelope.trim().is_empty() {
102 return Err(RpcError::Rpc("Transaction envelope is empty".to_string()));
103 }
104
105 let request = SimulateTransactionRequest {
106 transaction: envelope.to_string(),
107 };
108
109 let response: SimulateResponse = client.request("simulateTransaction", request).await?;
110
111 Ok(response)
112}
113
114pub fn validate_envelope(envelope: &str) -> Result<(), RpcError> {
117 if envelope.trim().is_empty() {
118 return Err(RpcError::Rpc("Transaction envelope is empty".to_string()));
119 }
120 Ok(())
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_request_serialization() {
129 let request = SimulateTransactionRequest {
130 transaction: "AAAAEnvelope===".to_string(),
131 };
132 let json = serde_json::to_value(request).unwrap();
133 assert_eq!(json["transaction"], "AAAAEnvelope===");
134 }
135
136 #[test]
137 fn test_response_deserialize_full() {
138 let raw = r#"{
139 "transactionData": "AAAAAdata",
140 "minResourceFee": "1000",
141 "restorePreamble": {
142 "minResourceFee": "500",
143 "transactionData": "AAAABdata"
144 },
145 "results": [
146 {"auth": ["AAAAauth1"], "xdr": "AAAAxdr1"}
147 ],
148 "cost": {"cpuInsns": "5000", "memBytes": "2048"},
149 "latestLedger": "12345",
150 "events": [{"type": "diagnostic"}],
151 "stateChanges": [{"type": "ledgerEntry"}]
152 }"#;
153 let response: SimulateResponse = serde_json::from_str(raw).unwrap();
154 assert_eq!(response.transaction_data, "AAAAAdata");
155 assert_eq!(response.min_resource_fee, "1000");
156 let restore = response.restore_preamble.unwrap();
157 assert_eq!(restore.min_resource_fee, "500");
158 assert_eq!(restore.transaction_data, "AAAABdata");
159 assert_eq!(response.results.len(), 1);
160 assert_eq!(response.results[0].auth, vec!["AAAAauth1".to_string()]);
161 assert_eq!(response.results[0].xdr, "AAAAxdr1");
162 let cost = response.cost.unwrap();
163 assert_eq!(cost.cpu_insns, "5000");
164 assert_eq!(cost.mem_bytes, "2048");
165 assert_eq!(response.latest_ledger, Some("12345".to_string()));
166 assert_eq!(response.events.len(), 1);
167 assert_eq!(response.state_changes.len(), 1);
168 assert_eq!(response.error, None);
169 }
170
171 #[test]
172 fn test_response_deserialize_latest_ledger_as_integer() {
173 let raw = r#"{
175 "transactionData": "AAAAAdata",
176 "latestLedger": 4718305
177 }"#;
178 let response: SimulateResponse = serde_json::from_str(raw).unwrap();
179 assert_eq!(response.latest_ledger, Some("4718305".to_string()));
180 }
181
182 #[test]
183 fn test_response_deserialize_minimal() {
184 let raw = r#"{"transactionData": "AAAAAdata"}"#;
186 let response: SimulateResponse = serde_json::from_str(raw).unwrap();
187 assert_eq!(response.min_resource_fee, "");
188 assert!(response.restore_preamble.is_none());
189 assert!(response.results.is_empty());
190 assert!(response.cost.is_none());
191 assert!(response.events.is_empty());
192 assert!(response.state_changes.is_empty());
193 assert_eq!(response.error, None);
194 }
195
196 #[test]
197 fn test_response_deserialize_error_field() {
198 let raw = r#"{
199 "transactionData": "",
200 "error": "host function failed"
201 }"#;
202 let response: SimulateResponse = serde_json::from_str(raw).unwrap();
203 assert_eq!(response.error, Some("host function failed".to_string()));
204 }
205
206 #[test]
207 fn test_response_deserialize_malformed() {
208 let raw = r#"{"transactionData": }"#;
209 assert!(serde_json::from_str::<SimulateResponse>(raw).is_err());
210 }
211
212 #[test]
213 fn test_empty_envelope_rejected() {
214 assert!(validate_envelope(" ").is_err());
215 assert!(validate_envelope("").is_err());
216 assert!(validate_envelope("AAAAEnvelope===").is_ok());
217 }
218}