ic_test/evm/
mod.rs

1use std::{
2    borrow::Cow,
3    collections::BTreeMap,
4    io::Read,
5    sync::{Arc, Mutex},
6};
7
8use alloy::{
9    network::{Ethereum, EthereumWallet, Network, TransactionBuilder},
10    primitives::{Address, BlockNumber, Bytes, B256, U128, U256, U64},
11    providers::{
12        fillers::{
13            BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
14            WalletFiller,
15        },
16        EthCall, EthCallMany, Identity, PendingTransactionBuilder, Provider, ProviderBuilder,
17        ProviderCall, RootProvider, RpcWithBlock, SendableTx,
18    },
19    rpc::{
20        client::NoParams,
21        types::{
22            erc4337::TransactionConditional,
23            simulate::{SimulatePayload, SimulatedBlock},
24            AccessListResult, Bundle, EthCallResponse, SyncStatus, TransactionRequest,
25        },
26    },
27    signers::{
28        k256::{elliptic_curve::SecretKey, Secp256k1},
29        local::PrivateKeySigner,
30    },
31    transports::TransportResult,
32};
33use alloy_node_bindings::{Anvil, AnvilInstance};
34use reqwest::Url;
35use serde_json::{json, value::RawValue};
36
37/// Represents a local Ethereum environment backed by an Anvil node.
38pub struct Evm {
39    rpc_url: Url,
40    anvil: AnvilInstance,
41    // This mutex is intentionally a sync mutex and not a tokio mutex.
42    users: std::sync::Mutex<BTreeMap<Address, EvmUser>>,
43}
44
45impl Evm {
46    /// New Evm environment
47    pub fn new() -> Self {
48        Evm::default()
49    }
50
51    /// HTTP RPC URL oif the Anvil instance
52    pub fn rpc_url(&self) -> Url {
53        self.rpc_url.clone()
54    }
55
56    /// Chain id of the current Anvil instance
57    pub fn chain_id(&self) -> u64 {
58        self.anvil.chain_id()
59    }
60
61    /// Number of test users
62    pub fn test_user_count(&self) -> usize {
63        self.anvil.addresses().len()
64    }
65
66    /// Return the `SecretKey` for a test account at the given index.
67    ///
68    /// # Panics
69    /// Panics if the index is out of bounds.
70    pub fn key(&self, index: usize) -> SecretKey<Secp256k1> {
71        self.anvil.keys()[index].clone()
72    }
73
74    /// Return a test user at a given index.
75    pub fn test_user(&self, index: usize) -> EvmUser {
76        if index >= self.test_user_count() {
77            panic!(
78                "Reached maximum number of test users: {}",
79                self.test_user_count()
80            );
81        }
82        self.user_from(
83            self.anvil.addresses()[index],
84            self.anvil.keys()[index].clone(),
85        )
86    }
87
88    /// Construct or retrieve an `EvmUser` for a given address and key.
89    pub fn user_from(&self, address: Address, key: SecretKey<Secp256k1>) -> EvmUser {
90        let mut users = self.users.lock().unwrap();
91        if let Some(user) = users.get(&address) {
92            return user.clone();
93        }
94        let signer: PrivateKeySigner = key.clone().into();
95        let provider = ProviderBuilder::new()
96            .wallet(EthereumWallet::from(signer))
97            .connect_http(self.rpc_url.clone());
98        let user = EvmUser {
99            address,
100            key,
101            provider: Arc::new(provider),
102        };
103        users.insert(user.address, user.clone());
104        user
105    }
106
107    /// First test user
108    pub fn default_user(&self) -> EvmUser {
109        self.test_user(0)
110    }
111
112    /// Send ETH from a user to another address.
113    pub async fn transfer(&self, user: &EvmUser, to: Address, amount: U256) {
114        let tx = TransactionRequest::default().with_to(to).with_value(amount);
115        user.provider
116            .send_transaction(tx)
117            .await
118            .unwrap()
119            .get_receipt()
120            .await
121            .unwrap();
122    }
123
124    /// Query the ETH balance of the given address.
125    pub async fn get_balance(&self, addr: Address) -> U256 {
126        self.default_user()
127            .provider
128            .get_balance(addr)
129            .await
130            .unwrap()
131    }
132
133    /// Mine a single block manually via `evm_mine`.
134    pub async fn mine_block(&self) {
135        let response: serde_json::Value = self
136            .default_user()
137            .provider
138            .client()
139            .request("evm_mine", json!({}))
140            .await
141            .unwrap();
142        assert_eq!(response, "0x0");
143    }
144}
145
146impl Default for Evm {
147    /// Initialize the Anvil test environment and capture its logs.
148    fn default() -> Self {
149        let mut anvil = Anvil::new().keep_stdout().try_spawn().unwrap();
150        let anvil_stdout = anvil.child_mut().stdout.take();
151
152        tokio::spawn(async {
153            let mut buf = [0_u8; 4096];
154            let mut mv = anvil_stdout;
155            loop {
156                tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
157                match mv.as_mut().unwrap().read(&mut buf) {
158                    Ok(len) => {
159                        eprintln!(
160                            "{}",
161                            String::from_utf8(buf[0..len].to_vec()).unwrap_or_default()
162                        );
163                    }
164                    Err(_) => return,
165                }
166            }
167        });
168
169        let anvil_url: Url = anvil.endpoint().parse().unwrap();
170        Self {
171            rpc_url: anvil_url,
172            anvil,
173            users: Mutex::new(BTreeMap::new()),
174        }
175    }
176}
177
178/// A fully configured EVM provider with wallet, nonce, gas, and chain ID fillers.
179pub type EvmProvider = FillProvider<
180    JoinFill<
181        JoinFill<
182            Identity,
183            JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
184        >,
185        WalletFiller<EthereumWallet>,
186    >,
187    RootProvider,
188    Ethereum,
189>;
190
191/// Represents a test user (address + provider + signer) in the EVM environment.
192#[derive(Clone)]
193pub struct EvmUser {
194    /// Ethereum address of the user.
195    pub address: Address,
196    /// Secret key used for signing transactions.
197    pub key: SecretKey<Secp256k1>,
198    /// EVM provider configured for this user.
199    pub provider: Arc<EvmProvider>,
200}
201
202#[async_trait::async_trait]
203impl Provider<Ethereum> for EvmUser {
204    fn root(&self) -> &RootProvider {
205        self.provider.root()
206    }
207
208    fn get_accounts(&self) -> ProviderCall<NoParams, Vec<Address>> {
209        self.provider.get_accounts()
210    }
211
212    fn get_blob_base_fee(&self) -> ProviderCall<NoParams, U128, u128> {
213        self.provider.get_blob_base_fee()
214    }
215
216    fn get_block_number(&self) -> ProviderCall<NoParams, U64, BlockNumber> {
217        self.provider.get_block_number()
218    }
219
220    fn call<'req>(
221        &self,
222        tx: <Ethereum as Network>::TransactionRequest,
223    ) -> EthCall<Ethereum, Bytes> {
224        self.provider.call(tx)
225    }
226
227    fn call_many<'req>(
228        &self,
229        bundles: &'req [Bundle],
230    ) -> EthCallMany<'req, Ethereum, Vec<Vec<EthCallResponse>>> {
231        self.provider.call_many(bundles)
232    }
233
234    fn simulate<'req>(
235        &self,
236        payload: &'req SimulatePayload,
237    ) -> RpcWithBlock<
238        &'req SimulatePayload,
239        Vec<SimulatedBlock<<Ethereum as Network>::BlockResponse>>,
240    > {
241        self.provider.simulate(payload)
242    }
243
244    fn get_chain_id(&self) -> ProviderCall<NoParams, U64, u64> {
245        self.provider.get_chain_id()
246    }
247
248    fn create_access_list<'a>(
249        &self,
250        request: &'a <Ethereum as Network>::TransactionRequest,
251    ) -> RpcWithBlock<&'a <Ethereum as Network>::TransactionRequest, AccessListResult> {
252        self.provider.create_access_list(request)
253    }
254
255    async fn send_raw_transaction(
256        &self,
257        encoded_tx: &[u8],
258    ) -> TransportResult<PendingTransactionBuilder<Ethereum>> {
259        self.provider.send_raw_transaction(encoded_tx).await
260    }
261
262    async fn send_raw_transaction_conditional(
263        &self,
264        encoded_tx: &[u8],
265        conditional: TransactionConditional,
266    ) -> TransportResult<PendingTransactionBuilder<Ethereum>> {
267        self.provider
268            .send_raw_transaction_conditional(encoded_tx, conditional)
269            .await
270    }
271
272    async fn send_transaction_internal(
273        &self,
274        tx: SendableTx<Ethereum>,
275    ) -> TransportResult<PendingTransactionBuilder<Ethereum>> {
276        self.provider.send_transaction_internal(tx).await
277    }
278
279    fn syncing(&self) -> ProviderCall<NoParams, SyncStatus> {
280        self.provider.syncing()
281    }
282
283    fn get_client_version(&self) -> ProviderCall<NoParams, String> {
284        self.provider.get_client_version()
285    }
286
287    fn get_sha3(&self, data: &[u8]) -> ProviderCall<(String,), B256> {
288        self.provider.get_sha3(data)
289    }
290
291    fn get_net_version(&self) -> ProviderCall<NoParams, U64, u64> {
292        self.provider.get_net_version()
293    }
294
295    async fn raw_request_dyn(
296        &self,
297        method: Cow<'static, str>,
298        params: &RawValue,
299    ) -> TransportResult<Box<RawValue>> {
300        self.provider.raw_request_dyn(method, params).await
301    }
302
303    fn transaction_request(&self) -> <Ethereum as Network>::TransactionRequest {
304        self.provider.transaction_request()
305    }
306}