base_simulacrum/
paymaster.rs1use alloy::primitives::U256;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::types::Call;
11
12#[derive(Error, Debug)]
13pub enum PaymasterError {
14 #[error("Gas estimation failed: {0}")]
15 EstimationFailed(String),
16 #[error("Sponsorship not available for this transaction")]
17 SponsorshipUnavailable,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct GasEstimate {
22 pub total_gas: U256,
23 pub per_call_gas: Vec<U256>,
24 pub sponsored: bool,
25 pub sponsor_signature: Option<String>,
26}
27
28pub struct LocalPaymaster {
29 base_gas_per_call: u64,
30 sponsorship_enabled: bool,
31}
32
33impl LocalPaymaster {
34 pub fn new(sponsorship_enabled: bool) -> Self {
35 Self {
36 base_gas_per_call: 21000,
37 sponsorship_enabled,
38 }
39 }
40
41 pub async fn estimate_batch_gas(&self, calls: &[Call]) -> Result<GasEstimate, PaymasterError> {
42 let mut per_call_gas = Vec::new();
43 let mut total_gas = U256::ZERO;
44
45 for call in calls {
46 let estimated = self.estimate_single_call(call).await?;
47 per_call_gas.push(estimated);
48 total_gas += estimated;
49 }
50
51 let batch_overhead = U256::from(5000 * calls.len() as u64);
52 total_gas += batch_overhead;
53
54 let sponsor_signature = if self.sponsorship_enabled {
55 Some(self.generate_mock_sponsor_signature(&total_gas))
56 } else {
57 None
58 };
59
60 Ok(GasEstimate {
61 total_gas,
62 per_call_gas,
63 sponsored: self.sponsorship_enabled,
64 sponsor_signature,
65 })
66 }
67
68 async fn estimate_single_call(&self, call: &Call) -> Result<U256, PaymasterError> {
69 if let Some(gas) = call.gas {
70 return Ok(gas);
71 }
72
73 let base = U256::from(self.base_gas_per_call);
74 let data_cost = U256::from(call.data.len() as u64 * 16);
75 let value_cost = if call.value.is_some() {
76 U256::from(9000)
77 } else {
78 U256::ZERO
79 };
80
81 Ok(base + data_cost + value_cost)
82 }
83
84 fn generate_mock_sponsor_signature(&self, total_gas: &U256) -> String {
85 let mock_sig = format!(
86 "0xsponsor_{:x}_mock_signature_v1",
87 total_gas.to_string().len()
88 );
89 mock_sig
90 }
91
92 pub fn apply_sponsorship(&self, estimate: &mut GasEstimate) {
93 if self.sponsorship_enabled {
94 estimate.sponsored = true;
95 estimate.sponsor_signature = Some(self.generate_mock_sponsor_signature(&estimate.total_gas));
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use alloy::primitives::{address, Bytes};
104
105 #[tokio::test]
106 async fn test_gas_estimation() {
107 let paymaster = LocalPaymaster::new(true);
108 let call = Call {
109 to: address!("0000000000000000000000000000000000000001"),
110 data: Bytes::from(vec![0u8; 100]),
111 value: None,
112 gas: None,
113 };
114
115 let estimate = paymaster.estimate_batch_gas(&[call]).await.unwrap();
116 assert!(estimate.total_gas > U256::ZERO);
117 assert!(estimate.sponsored);
118 assert!(estimate.sponsor_signature.is_some());
119 }
120}