base_simulacrum/
engine.rs1use alloy::primitives::U256;
7use alloy::providers::{Provider, ProviderBuilder};
8use alloy::rpc::types::TransactionRequest;
9use std::collections::HashMap;
10use std::sync::Arc;
11use thiserror::Error;
12use tokio::sync::RwLock;
13use uuid::Uuid;
14
15use crate::paymaster::{LocalPaymaster, PaymasterError};
16use crate::types::{BatchExecution, CallStatus, CallStatusResponse, Receipt, SendCallsParams};
17
18#[derive(Error, Debug)]
19pub enum EngineError {
20 #[error("Batch execution not found: {0}")]
21 BatchNotFound(String),
22 #[error("RPC error: {0}")]
23 RpcError(String),
24 #[error("Transaction failed: {0}")]
25 TransactionFailed(String),
26 #[error("Paymaster error: {0}")]
27 PaymasterError(#[from] PaymasterError),
28 #[error("Invalid parameters: {0}")]
29 InvalidParams(String),
30}
31
32pub struct Eip5792Engine {
33 rpc_url: String,
34 paymaster: LocalPaymaster,
35 executions: Arc<RwLock<HashMap<Uuid, BatchExecution>>>,
36}
37
38impl Eip5792Engine {
39 pub fn new(rpc_url: String, gas_sponsorship: bool) -> Self {
40 Self {
41 rpc_url,
42 paymaster: LocalPaymaster::new(gas_sponsorship),
43 executions: Arc::new(RwLock::new(HashMap::new())),
44 }
45 }
46
47 pub async fn wallet_send_calls(&self, params: SendCallsParams) -> Result<String, EngineError> {
48 println!(" Processing batch with {} calls", params.calls.len());
49
50 let sponsored = params
51 .capabilities
52 .as_ref()
53 .and_then(|c| c.get("paymasterService"))
54 .is_some();
55
56 let gas_estimate = self.paymaster.estimate_batch_gas(¶ms.calls).await?;
57
58 if sponsored {
59 println!(
60 " Gas sponsorship enabled (estimated: {} gas)",
61 gas_estimate.total_gas
62 );
63 }
64
65 let execution = BatchExecution::new(params.clone(), sponsored);
66 let batch_id = execution.id;
67
68 {
69 let mut executions = self.executions.write().await;
70 executions.insert(batch_id, execution);
71 }
72
73 let receipts = self.execute_calls(¶ms, &gas_estimate.per_call_gas).await?;
74
75 {
76 let mut executions = self.executions.write().await;
77 if let Some(exec) = executions.get_mut(&batch_id) {
78 exec.status = CallStatus::Confirmed;
79 exec.receipts = receipts;
80 }
81 }
82
83 Ok(format!("0x{}", batch_id.simple()))
84 }
85
86 async fn execute_calls(
87 &self,
88 params: &SendCallsParams,
89 gas_estimates: &[U256],
90 ) -> Result<Vec<Receipt>, EngineError> {
91 let provider = ProviderBuilder::new()
92 .on_http(self.rpc_url.parse().map_err(|e| {
93 EngineError::RpcError(format!("Invalid RPC URL: {}", e))
94 })?);
95
96 let mut receipts = Vec::new();
97
98 for (idx, call) in params.calls.iter().enumerate() {
99 println!(" → Executing call {}/{} to {}", idx + 1, params.calls.len(), call.to);
100
101 let gas_limit = gas_estimates
102 .get(idx)
103 .copied()
104 .unwrap_or(U256::from(100000))
105 .to::<u64>();
106
107 let tx = TransactionRequest::default()
108 .from(params.from)
109 .to(call.to)
110 .input(call.data.clone().into())
111 .value(call.value.unwrap_or(U256::ZERO))
112 .gas_limit(gas_limit);
113
114 let pending = provider
115 .send_transaction(tx)
116 .await
117 .map_err(|e| EngineError::RpcError(format!("Failed to send transaction: {}", e)))?;
118
119 let tx_hash = *pending.tx_hash();
120
121 let receipt = pending
122 .get_receipt()
123 .await
124 .map_err(|e| EngineError::RpcError(format!("Failed to get receipt: {}", e)))?;
125
126 let status = if receipt.status() { "0x1" } else { "0x0" };
127
128 receipts.push(Receipt {
129 logs: Vec::new(),
130 status: status.to_string(),
131 block_hash: format!("{:?}", receipt.block_hash.unwrap_or_default()),
132 block_number: format!("0x{:x}", receipt.block_number.unwrap_or_default()),
133 gas_used: format!("0x{:x}", receipt.gas_used),
134 transaction_hash: format!("{:?}", tx_hash),
135 });
136
137 if !receipt.status() {
138 return Err(EngineError::TransactionFailed(format!(
139 "Call {} failed",
140 idx + 1
141 )));
142 }
143
144 println!(" ✓ Call {} confirmed (gas: {})", idx + 1, receipt.gas_used);
145 }
146
147 Ok(receipts)
148 }
149
150 pub async fn wallet_get_calls_status(&self, batch_id: &str) -> Result<CallStatusResponse, EngineError> {
151 let uuid = self.parse_batch_id(batch_id)?;
152
153 let executions = self.executions.read().await;
154 let execution = executions
155 .get(&uuid)
156 .ok_or_else(|| EngineError::BatchNotFound(batch_id.to_string()))?;
157
158 Ok(CallStatusResponse {
159 status: execution.status.clone(),
160 receipts: execution.receipts.clone(),
161 })
162 }
163
164 fn parse_batch_id(&self, batch_id: &str) -> Result<Uuid, EngineError> {
165 let id_str = batch_id.strip_prefix("0x").unwrap_or(batch_id);
166 Uuid::parse_str(id_str)
167 .map_err(|_| EngineError::InvalidParams(format!("Invalid batch ID: {}", batch_id)))
168 }
169
170 pub async fn list_executions(&self) -> Vec<(Uuid, CallStatus)> {
171 let executions = self.executions.read().await;
172 executions
173 .iter()
174 .map(|(id, exec)| (*id, exec.status.clone()))
175 .collect()
176 }
177}