1use std::fmt;
8
9use alloy_primitives::Address;
10use cow_chains::{chain::SupportedChainId, contracts::settlement_contract};
11use cow_errors::CowError;
12
13use super::encoder::SettlementEncoder;
14
15#[derive(Debug, Clone)]
31pub struct SimulationResult {
32 pub success: bool,
34 pub gas_used: u64,
36 pub return_data: Vec<u8>,
38}
39
40impl SimulationResult {
41 #[must_use]
53 pub const fn new(success: bool, gas_used: u64, return_data: Vec<u8>) -> Self {
54 Self { success, gas_used, return_data }
55 }
56
57 #[must_use]
63 pub const fn is_success(&self) -> bool {
64 self.success
65 }
66
67 #[must_use]
73 pub const fn is_revert(&self) -> bool {
74 !self.success
75 }
76}
77
78impl fmt::Display for SimulationResult {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 if self.success {
81 write!(f, "Success (gas: {})", self.gas_used)
82 } else {
83 write!(f, "Revert (gas: {}, data: {} bytes)", self.gas_used, self.return_data.len())
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
102pub struct TradeSimulator {
103 rpc_url: String,
105 client: reqwest::Client,
107 settlement: Address,
109}
110
111impl TradeSimulator {
112 #[allow(clippy::shadow_reuse, reason = "builder pattern chains naturally shadow")]
119 fn build_client() -> reqwest::Client {
120 let builder = reqwest::Client::builder();
121 #[cfg(not(target_arch = "wasm32"))]
122 let builder = builder.timeout(std::time::Duration::from_secs(30));
123 builder.build().unwrap_or_default()
124 }
125
126 #[must_use]
139 pub fn new(rpc_url: impl Into<String>, chain: SupportedChainId) -> Self {
140 Self {
141 rpc_url: rpc_url.into(),
142 client: Self::build_client(),
143 settlement: settlement_contract(chain),
144 }
145 }
146
147 #[must_use]
153 pub const fn settlement_address(&self) -> Address {
154 self.settlement
155 }
156
157 #[must_use]
163 pub fn rpc_url(&self) -> &str {
164 &self.rpc_url
165 }
166
167 pub async fn estimate_gas(&self, calldata: &[u8]) -> Result<u64, CowError> {
185 let to_hex = format!("{:#x}", self.settlement);
186 let data_hex = format!("0x{}", alloy_primitives::hex::encode(calldata));
187
188 let body = serde_json::json!({
189 "jsonrpc": "2.0",
190 "method": "eth_estimateGas",
191 "params": [{"to": to_hex, "data": data_hex}],
192 "id": 1u32
193 });
194
195 let resp = self.client.post(&self.rpc_url).json(&body).send().await?;
196
197 if !resp.status().is_success() {
198 let code = i64::from(resp.status().as_u16());
199 let msg = resp.text().await.unwrap_or_else(|_e| String::new());
200 return Err(CowError::Rpc { code, message: msg });
201 }
202
203 let rpc: RpcResponse = resp.json().await?;
204
205 if let Some(err) = rpc.error {
206 return Err(CowError::Rpc { code: err.code, message: err.message });
207 }
208
209 let hex_str = rpc
210 .result
211 .ok_or_else(|| CowError::Rpc { code: -1, message: "missing result field".into() })?;
212
213 parse_hex_u64(&hex_str)
214 }
215
216 pub async fn simulate(&self, calldata: &[u8]) -> Result<SimulationResult, CowError> {
235 let to_hex = format!("{:#x}", self.settlement);
236 let data_hex = format!("0x{}", alloy_primitives::hex::encode(calldata));
237
238 let body = serde_json::json!({
239 "jsonrpc": "2.0",
240 "method": "eth_call",
241 "params": [{"to": to_hex, "data": data_hex}, "latest"],
242 "id": 1u32
243 });
244
245 let resp = self.client.post(&self.rpc_url).json(&body).send().await?;
246
247 if !resp.status().is_success() {
248 let code = i64::from(resp.status().as_u16());
249 let msg = resp.text().await.unwrap_or_else(|_e| String::new());
250 return Err(CowError::Rpc { code, message: msg });
251 }
252
253 let rpc: RpcResponse = resp.json().await?;
254
255 if let Some(err) = rpc.error {
256 return Ok(SimulationResult::new(false, 0, err.message.into_bytes()));
259 }
260
261 let hex_str = rpc
262 .result
263 .ok_or_else(|| CowError::Rpc { code: -1, message: "missing result field".into() })?;
264
265 let return_data = decode_hex_result(&hex_str)?;
266
267 let gas_used = self.estimate_gas(calldata).await.unwrap_or_default();
269
270 Ok(SimulationResult::new(true, gas_used, return_data))
271 }
272
273 pub async fn estimate_settlement(&self, encoder: &SettlementEncoder) -> Result<u64, CowError> {
291 let calldata = encoder.encode_settlement();
292 self.estimate_gas(&calldata).await
293 }
294}
295
296#[derive(serde::Deserialize)]
299struct RpcResponse {
300 result: Option<String>,
301 error: Option<RpcError>,
302}
303
304#[derive(serde::Deserialize)]
305struct RpcError {
306 code: i64,
307 message: String,
308}
309
310fn parse_hex_u64(hex_str: &str) -> Result<u64, CowError> {
314 let clean = hex_str.trim_start_matches("0x");
315 u64::from_str_radix(clean, 16)
316 .map_err(|e| CowError::Parse { field: "gas_estimate", reason: format!("invalid hex: {e}") })
317}
318
319fn decode_hex_result(hex_str: &str) -> Result<Vec<u8>, CowError> {
321 let clean = hex_str.trim_start_matches("0x");
322 alloy_primitives::hex::decode(clean)
323 .map_err(|e| CowError::Rpc { code: -1, message: format!("hex decode: {e}") })
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use cow_chains::contracts::SETTLEMENT_CONTRACT;
330
331 #[test]
334 fn simulation_result_new() {
335 let result = SimulationResult::new(true, 100_000, vec![0xab, 0xcd]);
336 assert!(result.success);
337 assert_eq!(result.gas_used, 100_000);
338 assert_eq!(result.return_data, vec![0xab, 0xcd]);
339 }
340
341 #[test]
342 fn simulation_result_is_success() {
343 let success = SimulationResult::new(true, 50_000, vec![]);
344 assert!(success.is_success());
345 assert!(!success.is_revert());
346 }
347
348 #[test]
349 fn simulation_result_is_revert() {
350 let revert = SimulationResult::new(false, 0, vec![0xff]);
351 assert!(!revert.is_success());
352 assert!(revert.is_revert());
353 }
354
355 #[test]
356 fn simulation_result_display_success() {
357 let result = SimulationResult::new(true, 150_000, vec![]);
358 assert_eq!(format!("{result}"), "Success (gas: 150000)");
359 }
360
361 #[test]
362 fn simulation_result_display_revert() {
363 let result = SimulationResult::new(false, 21_000, vec![0xde, 0xad]);
364 assert_eq!(format!("{result}"), "Revert (gas: 21000, data: 2 bytes)");
365 }
366
367 #[test]
368 fn simulation_result_clone() {
369 let result = SimulationResult::new(true, 42, vec![1, 2, 3]);
370 let cloned = result.clone();
371 assert_eq!(cloned.success, result.success);
372 assert_eq!(cloned.gas_used, result.gas_used);
373 assert_eq!(cloned.return_data, result.return_data);
374 }
375
376 #[test]
379 fn trade_simulator_new_mainnet() {
380 let sim = TradeSimulator::new("https://eth.example.com", SupportedChainId::Mainnet);
381 assert_eq!(sim.settlement_address(), SETTLEMENT_CONTRACT);
382 assert_eq!(sim.rpc_url(), "https://eth.example.com");
383 }
384
385 #[test]
386 fn trade_simulator_new_sepolia() {
387 let sim = TradeSimulator::new("https://sepolia.example.com", SupportedChainId::Sepolia);
388 assert_eq!(sim.settlement_address(), settlement_contract(SupportedChainId::Sepolia));
389 assert_eq!(sim.rpc_url(), "https://sepolia.example.com");
390 }
391
392 #[test]
393 fn trade_simulator_new_gnosis() {
394 let sim = TradeSimulator::new("https://gnosis.example.com", SupportedChainId::GnosisChain);
395 assert_eq!(sim.settlement_address(), settlement_contract(SupportedChainId::GnosisChain));
396 }
397
398 #[test]
399 fn trade_simulator_new_arbitrum() {
400 let sim = TradeSimulator::new("https://arb.example.com", SupportedChainId::ArbitrumOne);
401 assert_eq!(sim.settlement_address(), settlement_contract(SupportedChainId::ArbitrumOne));
402 }
403
404 #[test]
405 fn trade_simulator_clone() {
406 let sim = TradeSimulator::new("https://example.com", SupportedChainId::Mainnet);
407 let cloned = sim.clone();
408 assert_eq!(cloned.settlement_address(), sim.settlement_address());
409 assert_eq!(cloned.rpc_url(), sim.rpc_url());
410 }
411
412 #[test]
415 fn parse_hex_u64_valid() {
416 assert_eq!(parse_hex_u64("0x5208").unwrap(), 21_000);
417 }
418
419 #[test]
420 fn parse_hex_u64_no_prefix() {
421 assert_eq!(parse_hex_u64("ff").unwrap(), 255);
422 }
423
424 #[test]
425 fn parse_hex_u64_invalid() {
426 assert!(parse_hex_u64("0xZZZZ").is_err());
427 }
428
429 #[test]
430 fn decode_hex_result_valid() {
431 let bytes = decode_hex_result("0xdeadbeef").unwrap();
432 assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
433 }
434
435 #[test]
436 fn decode_hex_result_empty() {
437 let bytes = decode_hex_result("0x").unwrap();
438 assert!(bytes.is_empty());
439 }
440}