use alloy::primitives::U256;
use alloy::providers::{Provider, ProviderBuilder};
use alloy::rpc::types::TransactionRequest;
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::paymaster::{LocalPaymaster, PaymasterError};
use crate::types::{BatchExecution, CallStatus, CallStatusResponse, Receipt, SendCallsParams};
#[derive(Error, Debug)]
pub enum EngineError {
#[error("Batch execution not found: {0}")]
BatchNotFound(String),
#[error("RPC error: {0}")]
RpcError(String),
#[error("Transaction failed: {0}")]
TransactionFailed(String),
#[error("Paymaster error: {0}")]
PaymasterError(#[from] PaymasterError),
#[error("Invalid parameters: {0}")]
InvalidParams(String),
}
pub struct Eip5792Engine {
rpc_url: String,
paymaster: LocalPaymaster,
executions: Arc<RwLock<HashMap<Uuid, BatchExecution>>>,
}
impl Eip5792Engine {
pub fn new(rpc_url: String, gas_sponsorship: bool) -> Self {
Self {
rpc_url,
paymaster: LocalPaymaster::new(gas_sponsorship),
executions: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn wallet_send_calls(&self, params: SendCallsParams) -> Result<String, EngineError> {
println!(" Processing batch with {} calls", params.calls.len());
let sponsored = params
.capabilities
.as_ref()
.and_then(|c| c.get("paymasterService"))
.is_some();
let gas_estimate = self.paymaster.estimate_batch_gas(¶ms.calls).await?;
if sponsored {
println!(
" Gas sponsorship enabled (estimated: {} gas)",
gas_estimate.total_gas
);
}
let execution = BatchExecution::new(params.clone(), sponsored);
let batch_id = execution.id;
{
let mut executions = self.executions.write().await;
executions.insert(batch_id, execution);
}
let receipts = self.execute_calls(¶ms, &gas_estimate.per_call_gas).await?;
{
let mut executions = self.executions.write().await;
if let Some(exec) = executions.get_mut(&batch_id) {
exec.status = CallStatus::Confirmed;
exec.receipts = receipts;
}
}
Ok(format!("0x{}", batch_id.simple()))
}
async fn execute_calls(
&self,
params: &SendCallsParams,
gas_estimates: &[U256],
) -> Result<Vec<Receipt>, EngineError> {
let provider = ProviderBuilder::new()
.on_http(self.rpc_url.parse().map_err(|e| {
EngineError::RpcError(format!("Invalid RPC URL: {}", e))
})?);
let mut receipts = Vec::new();
for (idx, call) in params.calls.iter().enumerate() {
println!(" → Executing call {}/{} to {}", idx + 1, params.calls.len(), call.to);
let gas_limit = gas_estimates
.get(idx)
.copied()
.unwrap_or(U256::from(100000))
.to::<u64>();
let tx = TransactionRequest::default()
.from(params.from)
.to(call.to)
.input(call.data.clone().into())
.value(call.value.unwrap_or(U256::ZERO))
.gas_limit(gas_limit);
let pending = provider
.send_transaction(tx)
.await
.map_err(|e| EngineError::RpcError(format!("Failed to send transaction: {}", e)))?;
let tx_hash = *pending.tx_hash();
let receipt = pending
.get_receipt()
.await
.map_err(|e| EngineError::RpcError(format!("Failed to get receipt: {}", e)))?;
let status = if receipt.status() { "0x1" } else { "0x0" };
receipts.push(Receipt {
logs: Vec::new(),
status: status.to_string(),
block_hash: format!("{:?}", receipt.block_hash.unwrap_or_default()),
block_number: format!("0x{:x}", receipt.block_number.unwrap_or_default()),
gas_used: format!("0x{:x}", receipt.gas_used),
transaction_hash: format!("{:?}", tx_hash),
});
if !receipt.status() {
return Err(EngineError::TransactionFailed(format!(
"Call {} failed",
idx + 1
)));
}
println!(" ✓ Call {} confirmed (gas: {})", idx + 1, receipt.gas_used);
}
Ok(receipts)
}
pub async fn wallet_get_calls_status(&self, batch_id: &str) -> Result<CallStatusResponse, EngineError> {
let uuid = self.parse_batch_id(batch_id)?;
let executions = self.executions.read().await;
let execution = executions
.get(&uuid)
.ok_or_else(|| EngineError::BatchNotFound(batch_id.to_string()))?;
Ok(CallStatusResponse {
status: execution.status.clone(),
receipts: execution.receipts.clone(),
})
}
fn parse_batch_id(&self, batch_id: &str) -> Result<Uuid, EngineError> {
let id_str = batch_id.strip_prefix("0x").unwrap_or(batch_id);
Uuid::parse_str(id_str)
.map_err(|_| EngineError::InvalidParams(format!("Invalid batch ID: {}", batch_id)))
}
pub async fn list_executions(&self) -> Vec<(Uuid, CallStatus)> {
let executions = self.executions.read().await;
executions
.iter()
.map(|(id, exec)| (*id, exec.status.clone()))
.collect()
}
}