Skip to main content

base_simulacrum/
engine.rs

1//! EIP-5792 implementation.
2//!
3//! This module implements the EIP-5792 Wallet Call API, providing `wallet_sendCalls`
4//! and `wallet_getCallsStatus` functionality for batch transaction execution.
5
6use 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(&params.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(&params, &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}