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;
23pub 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 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)), ..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)), ..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, },
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 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}