ic_solidity_bindgen/
web3_provider.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
use crate::context::Web3Context;
use crate::providers::{CallProvider, LogProvider, SendProvider};
use crate::types::EventLog;
use async_trait::async_trait;
use ic_web3_rs::contract::tokens::{Detokenize, Tokenize};
use ic_web3_rs::contract::Contract;
use ic_web3_rs::contract::Options;
use ic_web3_rs::ethabi::{RawLog, Topic, TopicFilter};
use ic_web3_rs::ic::{get_public_key, pubkey_to_address, KeyInfo};
use ic_web3_rs::transports::ic_http_client::CallOptions;
use ic_web3_rs::transports::ICHttp;
use ic_web3_rs::types::{Address, BlockId, BlockNumber, FilterBuilder, TransactionReceipt, H256, U256, U64};
use ic_web3_rs::Transport;
use std::collections::HashMap;
use std::future::Future;
use std::marker::Unpin;

const RPC_CALL_MAX_RETRY: u8 = 3;
/// Mostly exists to map to the new futures.
/// This is the "untyped" API which the generated types will use.
pub struct Web3Provider {
    contract: Contract<ICHttp>,
    context: Web3Context,
    rpc_call_max_retry: u8,
}

impl Web3Provider {
    pub fn contract(&self) -> ic_web3_rs::ethabi::Contract {
        self.contract.abi().clone()
    }
    async fn with_retry<T, E, Fut, F: FnMut() -> Fut>(&self, mut f: F) -> Result<T, E>
    where
        Fut: Future<Output = Result<T, E>>,
    {
        let mut count = 0;
        loop {
            let result = f().await;

            if result.is_ok() {
                break result;
            } else {
                if count > self.rpc_call_max_retry {
                    break result;
                }
                count += 1;
            }
        }
    }
}

#[async_trait]
impl CallProvider for Web3Provider {
    async fn call<O: Detokenize + Unpin + Send, Params: Tokenize + Send>(
        &self,
        name: &'static str,
        params: Params,
    ) -> Result<O, ic_web3_rs::Error> {
        match self
            .contract
            .query(
                name,
                params,
                Some(self.context.from()),
                Default::default(),
                None,
            )
            .await
        {
            Ok(v) => Ok(v),
            Err(e) => match e {
                ic_web3_rs::contract::Error::Api(e) => Err(e),
                // The other variants InvalidOutputType and Abi should be
                // prevented by the code gen. It is useful to convert the error
                // type to be restricted to the web3::Error type for a few
                // reasons. First, the web3::Error type (unlike the
                // web3::contract::Error type) implements Send. This makes it
                // usable in async methods. Also for consistency it's easier to
                // mix methods using both call and send to use the ? operator if
                // they have the same error type. It is the opinion of this
                // library that ABI sorts of errors are irrecoverable and should
                // panic anyway.
                e => panic!("The ABI is out of date. Name: {}. Inner: {}", name, e),
            },
        }
    }
}

pub fn default_derivation_key() -> Vec<u8> {
    ic_cdk::id().as_slice().to_vec()
}

async fn public_key(key_name: String) -> Result<Vec<u8>, String> {
    get_public_key(
        None,
        vec![default_derivation_key()],
        // tmp: this should be a random string
        key_name,
    )
    .await
}

fn to_ethereum_address(pub_key: Vec<u8>) -> Result<Address, String> {
    pubkey_to_address(&pub_key)
}

pub async fn ethereum_address(key_name: String) -> Result<Address, String> {
    let pub_key = public_key(key_name).await?;
    to_ethereum_address(pub_key)
}

fn event_sig<T: Transport>(contract: &Contract<T>, name: &str) -> Result<H256, String> {
    contract
        .abi()
        .event(name)
        .map(|e| e.signature())
        .map_err(|e| (format!("event {} not found in contract abi: {}", name, e)))
}

#[async_trait]
impl LogProvider for Web3Provider {
    async fn find(
        &self,
        event_name: &str,
        from: u64,
        to: u64,
        call_options: CallOptions,
    ) -> Result<HashMap<u64, Vec<EventLog>>, ic_web3_rs::Error> {
        let parser = self
            .contract
            .abi()
            .event(event_name)
            .map_err(|_| ic_web3_rs::Error::Internal)?;
        let logs = self
            .context
            .eth()
            .logs(
                FilterBuilder::default()
                    .from_block(BlockNumber::Number(from.into()))
                    .to_block(BlockNumber::Number(to.into()))
                    .address(vec![self.contract.address()])
                    .topic_filter(TopicFilter {
                        topic0: Topic::This(event_sig(&self.contract, event_name).unwrap()),
                        topic1: Topic::Any,
                        topic2: Topic::Any,
                        topic3: Topic::Any,
                    })
                    .build(),
                call_options,
            )
            .await?
            .into_iter()
            .filter(|log| !log.removed.unwrap_or_default())
            .filter(|log| log.transaction_index.is_some())
            .filter(|log| log.block_hash.is_some())
            .map(|log| EventLog {
                event: parser
                    .parse_log(RawLog {
                        data: log.data.0.clone(),
                        topics: log.topics.clone(),
                    })
                    .unwrap(),
                log,
            })
            .fold(HashMap::new(), |mut acc, event| {
                let block = event.log.block_number.unwrap().as_u64();
                let events = acc.entry(block).or_insert_with(Vec::new);
                events.push(event);
                acc
            });
        Ok(logs)
    }
}

impl Web3Provider {
    pub async fn build_eip_1559_tx_params(&self) -> Result<Options, ic_web3_rs::Error> {
        let eth = self.context.eth();
        let current_block = self.with_retry(||{
            eth.block(BlockId::Number(BlockNumber::Latest), CallOptions::default())
        }).await?.expect("block not found");
        let max_priority_fee_per_gas = self
            .with_retry(|| eth.max_priority_fee_per_gas(CallOptions::default()))
            .await?;
        let nonce = self
            .with_retry(|| {
                eth
                    .transaction_count(self.context.from(), None, CallOptions::default())
            })
            .await?;

        Ok(Options {
            max_fee_per_gas: Some(calc_max_fee_per_gas(max_priority_fee_per_gas, current_block.base_fee_per_gas.unwrap_or_default())),
            max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
            nonce: Some(nonce),
            transaction_type: Some(U64::from(2)), // EIP1559_TX_ID for default
            ..Default::default()
        })
    }
}

fn calc_max_fee_per_gas(max_priority_fee_per_gas: U256, base_fee_per_gas: U256) -> U256 {
    max_priority_fee_per_gas + (base_fee_per_gas * U256::from(2))
}

#[async_trait]
impl SendProvider for Web3Provider {
    type Out = H256;
    async fn send<Params: Tokenize + Send>(
        &self,
        func: &'static str,
        params: Params,
        options: Option<Options>,
    ) -> Result<Self::Out, ic_web3_rs::Error> {
        let canister_addr = ethereum_address(self.context.key_name().to_string()).await?;
        let call_option = match options {
            Some(options) => options,
            None => self.build_eip_1559_tx_params().await?,
        };

        self.contract
            .signed_call(
                func,
                params,
                call_option,
                hex::encode(canister_addr),
                KeyInfo {
                    derivation_path: vec![default_derivation_key()],
                    key_name: self.context.key_name().to_string(),
                    ecdsa_sign_cycles: None, // use default (is there a problem with prod_key?)
                },
                self.context.chain_id(),
            )
            .await
    }
}

impl Web3Provider {
    pub fn new(contract_address: Address, context: &Web3Context, json_abi: &[u8]) -> Self {
        let context = context.clone();

        // All of the ABIs are verified at compile time, so we can just unwrap here.
        // See also 4cd1038f-56f2-4cf2-8dbe-672da9006083
        let contract = Contract::from_json(context.eth(), contract_address, json_abi).unwrap();

        Self {
            contract,
            context,
            rpc_call_max_retry: RPC_CALL_MAX_RETRY,
        }
    }
    pub fn set_max_retry(&mut self, max_retry: u8) {
        self.rpc_call_max_retry = max_retry;
    }
}