ic_solidity_bindgen/
web3_provider.rs

1use crate::{
2    context::Web3Context,
3    providers::{CallProvider, LogProvider, SendProvider},
4    rpc_methods::EVMRpcMethod,
5    types::EventLog,
6};
7use async_trait::async_trait;
8use ic_web3_rs::{
9    api::Namespace,
10    contract::{
11        tokens::{Detokenize, Tokenize},
12        Contract, Options,
13    },
14    ethabi::{RawLog, Topic, TopicFilter},
15    ic::KeyInfo,
16    transports::{ic_http_client::CallOptions, ICHttp},
17    types::{Address, BlockId, BlockNumber, FeeHistory, FilterBuilder, H256, U256, U64},
18    BatchTransport, Transport,
19};
20use std::{collections::HashMap, future::Future, marker::Unpin};
21
22const RPC_CALL_MAX_RETRY: u8 = 3;
23/// Mostly exists to map to the new futures.
24/// This is the "untyped" API which the generated types will use.
25pub struct Web3Provider {
26    contract: Contract<ICHttp>,
27    context: Web3Context,
28    rpc_call_max_retry: u8,
29}
30
31impl Web3Provider {
32    pub fn contract(&self) -> ic_web3_rs::ethabi::Contract {
33        self.contract.abi().clone()
34    }
35    async fn with_retry<T, E, Fut, F: FnMut() -> Fut>(&self, mut f: F) -> Result<T, E>
36    where
37        Fut: Future<Output = Result<T, E>>,
38    {
39        let mut count = 0;
40        loop {
41            let result = f().await;
42
43            if result.is_ok() {
44                break result;
45            } else {
46                if count > self.rpc_call_max_retry {
47                    break result;
48                }
49                count += 1;
50            }
51        }
52    }
53}
54
55#[async_trait]
56impl CallProvider for Web3Provider {
57    async fn call<O: Detokenize + Unpin + Send, Params: Tokenize + Send>(
58        &self,
59        name: &'static str,
60        params: Params,
61    ) -> Result<O, ic_web3_rs::Error> {
62        match self
63            .contract
64            .query(
65                name,
66                params,
67                Some(self.context.from()),
68                Default::default(),
69                None,
70            )
71            .await
72        {
73            Ok(v) => Ok(v),
74            Err(e) => match e {
75                ic_web3_rs::contract::Error::Api(e) => Err(e),
76                // The other variants InvalidOutputType and Abi should be
77                // prevented by the code gen. It is useful to convert the error
78                // type to be restricted to the web3::Error type for a few
79                // reasons. First, the web3::Error type (unlike the
80                // web3::contract::Error type) implements Send. This makes it
81                // usable in async methods. Also for consistency it's easier to
82                // mix methods using both call and send to use the ? operator if
83                // they have the same error type. It is the opinion of this
84                // library that ABI sorts of errors are irrecoverable and should
85                // panic anyway.
86                e => panic!("The ABI is out of date. Name: {}. Inner: {}", name, e),
87            },
88        }
89    }
90}
91
92pub fn default_derivation_key() -> Vec<u8> {
93    ic_cdk::id().as_slice().to_vec()
94}
95
96fn event_sig<T: Transport>(contract: &Contract<T>, name: &str) -> Result<H256, String> {
97    contract
98        .abi()
99        .event(name)
100        .map(|e| e.signature())
101        .map_err(|e| (format!("event {} not found in contract abi: {}", name, e)))
102}
103
104#[async_trait]
105impl LogProvider for Web3Provider {
106    async fn find(
107        &self,
108        event_name: &str,
109        from: u64,
110        to: u64,
111        call_options: CallOptions,
112    ) -> Result<HashMap<u64, Vec<EventLog>>, ic_web3_rs::Error> {
113        let parser = self
114            .contract
115            .abi()
116            .event(event_name)
117            .map_err(|_| ic_web3_rs::Error::Internal)?;
118        let logs = self
119            .context
120            .eth()
121            .logs(
122                FilterBuilder::default()
123                    .from_block(BlockNumber::Number(from.into()))
124                    .to_block(BlockNumber::Number(to.into()))
125                    .address(vec![self.contract.address()])
126                    .topic_filter(TopicFilter {
127                        topic0: Topic::This(event_sig(&self.contract, event_name).unwrap()),
128                        topic1: Topic::Any,
129                        topic2: Topic::Any,
130                        topic3: Topic::Any,
131                    })
132                    .build(),
133                call_options,
134            )
135            .await?
136            .into_iter()
137            .filter(|log| !log.removed.unwrap_or_default())
138            .filter(|log| log.transaction_index.is_some())
139            .filter(|log| log.block_hash.is_some())
140            .map(|log| EventLog {
141                event: parser
142                    .parse_log(RawLog {
143                        data: log.data.0.clone(),
144                        topics: log.topics.clone(),
145                    })
146                    .unwrap(),
147                log,
148            })
149            .fold(HashMap::new(), |mut acc, event| {
150                let block = event.log.block_number.unwrap().as_u64();
151                let events = acc.entry(block).or_insert_with(Vec::new);
152                events.push(event);
153                acc
154            });
155        Ok(logs)
156    }
157}
158
159impl Web3Provider {
160    pub async fn build_eip_1559_tx_params(&self) -> Result<Options, ic_web3_rs::Error> {
161        let eth = self.context.eth();
162        let current_block = self
163            .with_retry(|| eth.block(BlockId::Number(BlockNumber::Latest), CallOptions::default()))
164            .await?;
165        if current_block.is_none() {
166            return Err(ic_web3_rs::Error::InvalidResponse(
167                "No block returned".to_string(),
168            ));
169        }
170        let current_block = current_block.unwrap();
171        self._build_eip_1559_tx_params(current_block.base_fee_per_gas.unwrap_or_default())
172            .await
173    }
174
175    pub async fn build_eip_1559_tx_params_with_fee_history(
176        &self,
177    ) -> Result<Options, ic_web3_rs::Error> {
178        let eth = self.context.eth();
179        let fee_history = self
180            .with_retry(|| {
181                eth.fee_history(
182                    U256::one(),
183                    BlockNumber::Latest,
184                    None,
185                    CallOptions::default(),
186                )
187            })
188            .await?;
189        self._build_eip_1559_tx_params(
190            fee_history
191                .base_fee_per_gas
192                .get(0)
193                .map(|f| *f)
194                .unwrap_or_default(),
195        )
196        .await
197    }
198
199    pub async fn build_eip_1559_tx_params_with_batch(&self) -> Result<Options, ic_web3_rs::Error> {
200        let requests = vec![
201            EVMRpcMethod::FeeHistory(U256::one(), BlockNumber::Latest, None),
202            EVMRpcMethod::MaxPriorityFeePerGas,
203            EVMRpcMethod::TransactionCount(self.context.from(), BlockNumber::Latest),
204        ];
205        let resp = self.batch_call(&requests).await?;
206
207        let (ok, err) = resp.into_iter().partition::<Vec<_>, _>(Result::is_ok);
208        if !err.is_empty() {
209            return Err(ic_web3_rs::error::Error::InvalidResponse(format!(
210                "Some method failed: {err:?}"
211            )));
212        }
213        if ok.len() != requests.len() {
214            return Err(ic_web3_rs::error::Error::InvalidResponse(format!(
215                "Some method not responded. response={ok:?}"
216            )));
217        }
218
219        let mut ok = ok.into_iter().filter_map(Result::ok).collect::<Vec<_>>();
220        let fee_history: FeeHistory = serde_json::from_value(ok.remove(0))?;
221        let base_fee_per_gas = fee_history
222            .base_fee_per_gas
223            .get(0)
224            .map(|f| *f)
225            .unwrap_or_default();
226        let max_priority_fee_per_gas: U256 = serde_json::from_value(ok.remove(0))?;
227        let nonce = serde_json::from_value(ok.remove(0))?;
228
229        Ok(Options {
230            max_fee_per_gas: Some(calc_max_fee_per_gas(
231                max_priority_fee_per_gas,
232                base_fee_per_gas,
233            )),
234            max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
235            nonce: Some(nonce),
236            transaction_type: Some(U64::from(2)), // EIP1559_TX_ID for default
237            ..Default::default()
238        })
239    }
240
241    pub async fn build_legacy_tx_params_with_batch(&self) -> Result<Options, ic_web3_rs::Error> {
242        let requests = vec![
243            EVMRpcMethod::GasPrice,
244            EVMRpcMethod::TransactionCount(self.context.from(), BlockNumber::Latest),
245        ];
246        let resp = self.batch_call(&requests).await?;
247
248        let (ok, err) = resp.into_iter().partition::<Vec<_>, _>(Result::is_ok);
249        if !err.is_empty() {
250            return Err(ic_web3_rs::error::Error::InvalidResponse(format!(
251                "Some method failed: {err:?}"
252            )));
253        }
254        if ok.len() != requests.len() {
255            return Err(ic_web3_rs::error::Error::InvalidResponse(format!(
256                "Some method not responded. response={ok:?}"
257            )));
258        }
259
260        let mut ok = ok.into_iter().filter_map(Result::ok).collect::<Vec<_>>();
261        let gas_price: U256 = serde_json::from_value(ok.remove(0))?;
262        let nonce = serde_json::from_value(ok.remove(0))?;
263
264        Ok(Options {
265            gas_price: Some(gas_price),
266            nonce: Some(nonce),
267            ..Default::default()
268        })
269    }
270
271    pub async fn estimate_gas<P>(
272        &self,
273        func: &str,
274        params: P,
275        from: Address,
276        options: Options,
277    ) -> Result<U256, ic_web3_rs::contract::Error>
278    where
279        P: Tokenize,
280    {
281        self.contract
282            .estimate_gas(func, params, from, options)
283            .await
284    }
285
286    pub async fn batch_call(
287        &self,
288        calls: &Vec<EVMRpcMethod>,
289    ) -> Result<Vec<Result<serde_json::Value, ic_web3_rs::Error>>, ic_web3_rs::Error> {
290        let transport = self.context.eth().transport();
291        let calls = calls
292            .into_iter()
293            .map(|c| transport.prepare(c.method(), c.params()))
294            .collect::<Vec<_>>();
295
296        transport.send_batch(calls).await
297    }
298
299    async fn _build_eip_1559_tx_params(
300        &self,
301        base_fee_per_gas: U256,
302    ) -> Result<Options, ic_web3_rs::Error> {
303        let eth = self.context.eth();
304        let max_priority_fee_per_gas = self
305            .with_retry(|| eth.max_priority_fee_per_gas(CallOptions::default()))
306            .await?;
307        let nonce = self
308            .with_retry(|| eth.transaction_count(self.context.from(), None, CallOptions::default()))
309            .await?;
310
311        Ok(Options {
312            max_fee_per_gas: Some(calc_max_fee_per_gas(
313                max_priority_fee_per_gas,
314                base_fee_per_gas,
315            )),
316            max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
317            nonce: Some(nonce),
318            transaction_type: Some(U64::from(2)), // EIP1559_TX_ID for default
319            ..Default::default()
320        })
321    }
322}
323
324fn calc_max_fee_per_gas(max_priority_fee_per_gas: U256, base_fee_per_gas: U256) -> U256 {
325    max_priority_fee_per_gas + (base_fee_per_gas * U256::from(2))
326}
327
328#[async_trait]
329impl SendProvider for Web3Provider {
330    type Out = (H256, Option<ic_web3_rs::Error>);
331    async fn send<Params: Tokenize + Send>(
332        &self,
333        func: &'static str,
334        params: Params,
335        options: Option<Options>,
336    ) -> Result<Self::Out, ic_web3_rs::Error> {
337        let canister_addr = self.context.from();
338        let call_option = match options {
339            Some(options) => options,
340            None => self.build_eip_1559_tx_params().await?,
341        };
342
343        let send_option = call_option.call_options;
344        let signed_tx = self
345            .contract
346            .sign(
347                func,
348                params,
349                Options {
350                    call_options: None,
351                    ..call_option
352                },
353                hex::encode(canister_addr),
354                KeyInfo {
355                    derivation_path: vec![default_derivation_key()],
356                    key_name: self.context.key_name().to_string(),
357                    ecdsa_sign_cycles: None, // use default (is there a problem with prod_key?)
358                },
359                self.context.chain_id(),
360            )
361            .await?;
362        let res = self
363            .context
364            .eth()
365            .send_raw_transaction(signed_tx.raw_transaction, send_option.unwrap_or_default())
366            .await;
367        Ok((signed_tx.transaction_hash, res.err()))
368    }
369}
370
371impl Web3Provider {
372    pub fn new(contract_address: Address, context: &Web3Context, json_abi: &[u8]) -> Self {
373        let context = context.clone();
374
375        // All of the ABIs are verified at compile time, so we can just unwrap here.
376        // See also 4cd1038f-56f2-4cf2-8dbe-672da9006083
377        let contract =
378            Contract::from_json(context.eth().clone(), contract_address, json_abi).unwrap();
379
380        Self {
381            contract,
382            context,
383            rpc_call_max_retry: RPC_CALL_MAX_RETRY,
384        }
385    }
386    pub fn set_max_retry(&mut self, max_retry: u8) {
387        self.rpc_call_max_retry = max_retry;
388    }
389}