Skip to main content

base_simulacrum/
paymaster.rs

1//! Local gas sponsorship simulator.
2//!
3//! This module provides mock paymaster functionality for testing gas-sponsored
4//! transactions without requiring a real paymaster service.
5
6use 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}