Skip to main content

linera_ethereum/test_utils/
mod.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use alloy::{
5    network::{Ethereum, EthereumWallet},
6    node_bindings::{Anvil, AnvilInstance},
7    providers::{
8        fillers::{
9            BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
10            WalletFiller,
11        },
12        ProviderBuilder, RootProvider,
13    },
14    signers::local::PrivateKeySigner,
15    sol,
16};
17use alloy_primitives::{Address, U256};
18use linera_base::port::get_free_port;
19use url::Url;
20
21use crate::{
22    client::EthereumQueries, common::EthereumServiceError, provider::EthereumClientSimplified,
23};
24
25sol!(
26    #[allow(missing_docs)]
27    #[sol(rpc)]
28    SimpleTokenContract,
29    "./contracts/SimpleToken.json"
30);
31
32sol!(
33    #[allow(missing_docs)]
34    #[sol(rpc)]
35    EventNumericsContract,
36    "./contracts/EventNumerics.json"
37);
38
39/// An Ethereum client wrapping a configured `alloy` provider, used in tests.
40#[expect(clippy::type_complexity)]
41#[derive(Clone)]
42pub struct EthereumClient {
43    /// The underlying `alloy` provider used to talk to the node.
44    pub provider: FillProvider<
45        JoinFill<
46            JoinFill<
47                alloy::providers::Identity,
48                JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
49            >,
50            WalletFiller<EthereumWallet>,
51        >,
52        RootProvider<Ethereum>,
53    >,
54}
55
56impl EthereumClient {
57    /// Connects to an existing Ethereum node and creates an `EthereumClient`
58    /// if successful.
59    pub fn new(url: &str) -> Result<Self, EthereumServiceError> {
60        let rpc_url = Url::parse(url)?;
61        // this address is in the anvil test.
62        let pk: PrivateKeySigner =
63            "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
64                .parse()
65                .unwrap();
66        let wallet = EthereumWallet::from(pk);
67        let provider = ProviderBuilder::new()
68            .wallet(wallet.clone())
69            .connect_http(rpc_url);
70        let endpoint = Self { provider };
71        Ok(endpoint)
72    }
73}
74
75/// A running Anvil node together with a client connected to it, for tests.
76pub struct AnvilTest {
77    /// The running Anvil instance.
78    pub anvil_instance: AnvilInstance,
79    /// The HTTP endpoint of the Anvil node.
80    pub endpoint: String,
81    /// A client connected to the Anvil node.
82    pub ethereum_client: EthereumClient,
83    /// The parsed RPC URL of the Anvil node.
84    pub rpc_url: Url,
85}
86
87/// Spawns a fresh Anvil node and returns an [`AnvilTest`] connected to it.
88pub async fn get_anvil() -> anyhow::Result<AnvilTest> {
89    let port = get_free_port().await?;
90    let anvil_instance = Anvil::new().port(port).try_spawn()?;
91    let endpoint = anvil_instance.endpoint();
92    let ethereum_client = EthereumClient::new(&endpoint)?;
93    let rpc_url = Url::parse(&endpoint)?;
94    Ok(AnvilTest {
95        anvil_instance,
96        endpoint,
97        ethereum_client,
98        rpc_url,
99    })
100}
101
102impl AnvilTest {
103    /// Returns the Anvil account address at the given `index` as a hexadecimal string.
104    pub fn get_address(&self, index: usize) -> String {
105        let address = self.anvil_instance.addresses()[index];
106        format!("{address:?}")
107    }
108}
109
110/// A deployed `SimpleToken` contract together with the Anvil node it lives on.
111pub struct SimpleTokenContractFunction {
112    /// The address of the deployed contract.
113    pub contract_address: String,
114    /// The Anvil node hosting the contract.
115    pub anvil_test: AnvilTest,
116}
117
118impl SimpleTokenContractFunction {
119    /// Deploys a new `SimpleToken` contract on `anvil_test` and returns a handle to it.
120    pub async fn new(anvil_test: AnvilTest) -> anyhow::Result<Self> {
121        // 2: initializing the contract
122        let initial_supply = U256::from(1000);
123        let simple_token =
124            SimpleTokenContract::deploy(&anvil_test.ethereum_client.provider, initial_supply)
125                .await?;
126        let contract_address = simple_token.address();
127        let contract_address = format!("{contract_address:?}");
128        Ok(Self {
129            contract_address,
130            anvil_test,
131        })
132    }
133
134    /// Returns the token balance of address `to` at the given `block`.
135    // Only the balanceOf operation is of interest for this contract
136    pub async fn balance_of(&self, to: &str, block: u64) -> anyhow::Result<U256> {
137        // Getting the simple_token
138        let contract_address = self.contract_address.parse::<Address>()?;
139        let simple_token = SimpleTokenContract::new(
140            contract_address,
141            self.anvil_test.ethereum_client.provider.clone(),
142        );
143        // Creating the calldata
144        let to_address = to.parse::<Address>()?;
145        let data = simple_token.balanceOf(to_address).calldata().clone();
146        // Using the Ethereum client simplified.
147        let ethereum_client_simp = EthereumClientSimplified::new(self.anvil_test.endpoint.clone());
148        let answer = ethereum_client_simp
149            .non_executive_call(&self.contract_address, data, to, block)
150            .await?;
151        // Converting the output
152        let mut vec = [0_u8; 32];
153        for (i, val) in vec.iter_mut().enumerate() {
154            *val = answer.0[i];
155        }
156        let balance = U256::from_be_bytes(vec);
157        Ok(balance)
158    }
159
160    /// Transfers `value` tokens from address `from` to address `to`.
161    pub async fn transfer(&self, from: &str, to: &str, value: U256) -> anyhow::Result<()> {
162        // Getting the simple_token
163        let contract_address = self.contract_address.parse::<Address>()?;
164        let to_address = to.parse::<Address>()?;
165        let from_address = from.parse::<Address>()?;
166        let simple_token = SimpleTokenContract::new(
167            contract_address,
168            self.anvil_test.ethereum_client.provider.clone(),
169        );
170        // Doing the transfer
171        let builder = simple_token.transfer(to_address, value).from(from_address);
172        let _receipt = builder.send().await?.get_receipt().await?;
173        Ok(())
174    }
175}
176
177/// A deployed `EventNumerics` contract together with the Anvil node it lives on.
178pub struct EventNumericsContractFunction {
179    /// The address of the deployed contract.
180    pub contract_address: String,
181    /// The Anvil node hosting the contract.
182    pub anvil_test: AnvilTest,
183}
184
185impl EventNumericsContractFunction {
186    /// Deploys a new `EventNumerics` contract on `anvil_test` and returns a handle to it.
187    pub async fn new(anvil_test: AnvilTest) -> anyhow::Result<Self> {
188        // Deploying the event numerics contract
189        let initial_supply = U256::from(0);
190        let event_numerics =
191            EventNumericsContract::deploy(&anvil_test.ethereum_client.provider, initial_supply)
192                .await?;
193        // Getting the contract address
194        let contract_address = event_numerics.address();
195        let contract_address = format!("{contract_address:?}");
196        Ok(Self {
197            contract_address,
198            anvil_test,
199        })
200    }
201}