ic_web3/contract/
mod.rs

1//! Ethereum Contract Interface
2
3use crate::{
4    api::{Eth, Namespace},
5    confirm,
6    contract::tokens::{Detokenize, Tokenize},
7    futures::Future,
8    types::{
9        AccessList, Address, BlockId, Bytes, CallRequest, FilterBuilder, TransactionCondition, TransactionReceipt,
10        TransactionRequest, H256, U256, U64,
11    },
12    Transport,
13    ic::KeyInfo,
14};
15use std::{collections::HashMap, hash::Hash, time};
16
17pub mod deploy;
18// pub mod ens;
19mod error;
20pub mod tokens;
21
22pub use crate::contract::error::Error;
23
24/// Contract `Result` type.
25pub type Result<T> = std::result::Result<T, Error>;
26
27/// Contract Call/Query Options
28#[derive(Default, Debug, Clone, PartialEq)]
29pub struct Options {
30    /// Fixed gas limit
31    pub gas: Option<U256>,
32    /// Fixed gas price
33    pub gas_price: Option<U256>,
34    /// Value to transfer
35    pub value: Option<U256>,
36    /// Fixed transaction nonce
37    pub nonce: Option<U256>,
38    /// A condition to satisfy before including transaction.
39    pub condition: Option<TransactionCondition>,
40    /// Transaction type, Some(1) for AccessList transaction, None for Legacy
41    pub transaction_type: Option<U64>,
42    /// Access list
43    pub access_list: Option<AccessList>,
44    /// Max fee per gas
45    pub max_fee_per_gas: Option<U256>,
46    /// miner bribe
47    pub max_priority_fee_per_gas: Option<U256>,
48}
49
50impl Options {
51    /// Create new default `Options` object with some modifications.
52    pub fn with<F>(func: F) -> Options
53    where
54        F: FnOnce(&mut Options),
55    {
56        let mut options = Options::default();
57        func(&mut options);
58        options
59    }
60}
61
62/// Ethereum Contract Interface
63#[derive(Debug, Clone)]
64pub struct Contract<T: Transport> {
65    address: Address,
66    eth: Eth<T>,
67    abi: ethabi::Contract,
68}
69
70impl<T: Transport> Contract<T> {
71    /// Creates deployment builder for a contract given it's ABI in JSON.
72    pub fn deploy(eth: Eth<T>, json: &[u8]) -> ethabi::Result<deploy::Builder<T>> {
73        let abi = ethabi::Contract::load(json)?;
74        Ok(deploy::Builder {
75            eth,
76            abi,
77            options: Options::default(),
78            confirmations: 1,
79            poll_interval: time::Duration::from_secs(7),
80            linker: HashMap::default(),
81        })
82    }
83
84    /// test
85    pub fn deploy_from_truffle<S>(
86        eth: Eth<T>,
87        json: &[u8],
88        linker: HashMap<S, Address>,
89    ) -> ethabi::Result<deploy::Builder<T>>
90    where
91        S: AsRef<str> + Eq + Hash,
92    {
93        let abi = ethabi::Contract::load(json)?;
94        let linker: HashMap<String, Address> = linker.into_iter().map(|(s, a)| (s.as_ref().to_string(), a)).collect();
95        Ok(deploy::Builder {
96            eth,
97            abi,
98            options: Options::default(),
99            confirmations: 1,
100            poll_interval: time::Duration::from_secs(7),
101            linker,
102        })
103    }
104}
105
106impl<T: Transport> Contract<T> {
107    /// Creates new Contract Interface given blockchain address and ABI
108    pub fn new(eth: Eth<T>, address: Address, abi: ethabi::Contract) -> Self {
109        Contract { address, eth, abi }
110    }
111
112    /// Creates new Contract Interface given blockchain address and JSON containing ABI
113    pub fn from_json(eth: Eth<T>, address: Address, json: &[u8]) -> ethabi::Result<Self> {
114        let abi = ethabi::Contract::load(json)?;
115        Ok(Self::new(eth, address, abi))
116    }
117
118    /// Get the underlying contract ABI.
119    pub fn abi(&self) -> &ethabi::Contract {
120        &self.abi
121    }
122
123    /// Returns contract address
124    pub fn address(&self) -> Address {
125        self.address
126    }
127
128    /// Execute a contract function
129    pub async fn call<P>(&self, func: &str, params: P, from: Address, options: Options) -> Result<H256>
130    where
131        P: Tokenize,
132    {
133        let data = self.abi.function(func)?.encode_input(&params.into_tokens())?;
134        let Options {
135            gas,
136            gas_price,
137            value,
138            nonce,
139            condition,
140            transaction_type,
141            access_list,
142            max_fee_per_gas,
143            max_priority_fee_per_gas,
144        } = options;
145        self.eth
146            .send_transaction(TransactionRequest {
147                from,
148                to: Some(self.address),
149                gas,
150                gas_price,
151                value,
152                nonce,
153                data: Some(Bytes(data)),
154                condition,
155                transaction_type,
156                access_list,
157                max_fee_per_gas,
158                max_priority_fee_per_gas,
159            })
160            .await
161            .map_err(Error::from)
162    }
163
164    /// Execute a contract function and wait for confirmations
165    pub async fn call_with_confirmations(
166        &self,
167        func: &str,
168        params: impl Tokenize,
169        from: Address,
170        options: Options,
171        confirmations: usize,
172    ) -> crate::error::Result<TransactionReceipt> {
173        let poll_interval = time::Duration::from_secs(1);
174
175        let fn_data = self
176            .abi
177            .function(func)
178            .and_then(|function| function.encode_input(&params.into_tokens()))
179            // TODO [ToDr] SendTransactionWithConfirmation should support custom error type (so that we can return
180            // `contract::Error` instead of more generic `Error`.
181            .map_err(|err| crate::error::Error::Decoder(format!("{:?}", err)))?;
182        let transaction_request = TransactionRequest {
183            from,
184            to: Some(self.address),
185            gas: options.gas,
186            gas_price: options.gas_price,
187            value: options.value,
188            nonce: options.nonce,
189            data: Some(Bytes(fn_data)),
190            condition: options.condition,
191            transaction_type: options.transaction_type,
192            access_list: options.access_list,
193            max_fee_per_gas: options.max_fee_per_gas,
194            max_priority_fee_per_gas: options.max_priority_fee_per_gas,
195        };
196        confirm::send_transaction_with_confirmation(
197            self.eth.transport().clone(),
198            transaction_request,
199            poll_interval,
200            confirmations,
201        )
202        .await
203    }
204
205    /// Estimate gas required for this function call.
206    pub async fn estimate_gas<P>(&self, func: &str, params: P, from: Address, options: Options) -> Result<U256>
207    where
208        P: Tokenize,
209    {
210        let data = self.abi.function(func)?.encode_input(&params.into_tokens())?;
211        self.eth
212            .estimate_gas(
213                CallRequest {
214                    from: Some(from),
215                    to: Some(self.address),
216                    gas: options.gas,
217                    gas_price: options.gas_price,
218                    value: options.value,
219                    data: Some(Bytes(data)),
220                    transaction_type: options.transaction_type,
221                    access_list: options.access_list,
222                    max_fee_per_gas: options.max_fee_per_gas,
223                    max_priority_fee_per_gas: options.max_priority_fee_per_gas,
224                },
225                None,
226            )
227            .await
228            .map_err(Into::into)
229    }
230
231    /// Call constant function
232    pub fn query<R, A, B, P>(
233        &self,
234        func: &str,
235        params: P,
236        from: A,
237        options: Options,
238        block: B,
239    ) -> impl Future<Output = Result<R>> + '_
240    where
241        R: Detokenize,
242        A: Into<Option<Address>>,
243        B: Into<Option<BlockId>>,
244        P: Tokenize,
245    {
246        let result = self
247            .abi
248            .function(func)
249            .and_then(|function| {
250                function
251                    .encode_input(&params.into_tokens())
252                    .map(|call| (call, function))
253            })
254            .map(|(call, function)| {
255                let call_future = self.eth.call(
256                    CallRequest {
257                        from: from.into(),
258                        to: Some(self.address),
259                        gas: options.gas,
260                        gas_price: options.gas_price,
261                        value: options.value,
262                        data: Some(Bytes(call)),
263                        transaction_type: options.transaction_type,
264                        access_list: options.access_list,
265                        max_fee_per_gas: options.max_fee_per_gas,
266                        max_priority_fee_per_gas: options.max_priority_fee_per_gas,
267                    },
268                    block.into(),
269                );
270                (call_future, function)
271            });
272        // NOTE for the batch transport to work correctly, we must call `transport.execute` without ever polling the future,
273        // hence it cannot be a fully `async` function.
274        async {
275            let (call_future, function) = result?;
276            let bytes = call_future.await?;
277            let output = function.decode_output(&bytes.0)?;
278            R::from_tokens(output)
279        }
280    }
281
282    /// Find events matching the topics.
283    pub async fn events<A, B, C, R>(&self, event: &str, topic0: A, topic1: B, topic2: C) -> Result<Vec<R>>
284    where
285        A: Tokenize,
286        B: Tokenize,
287        C: Tokenize,
288        R: Detokenize,
289    {
290        fn to_topic<A: Tokenize>(x: A) -> ethabi::Topic<ethabi::Token> {
291            let tokens = x.into_tokens();
292            if tokens.is_empty() {
293                ethabi::Topic::Any
294            } else {
295                tokens.into()
296            }
297        }
298
299        let res = self.abi.event(event).and_then(|ev| {
300            let filter = ev.filter(ethabi::RawTopicFilter {
301                topic0: to_topic(topic0),
302                topic1: to_topic(topic1),
303                topic2: to_topic(topic2),
304            })?;
305            Ok((ev.clone(), filter))
306        });
307        let (ev, filter) = match res {
308            Ok(x) => x,
309            Err(e) => return Err(e.into()),
310        };
311
312        let logs = self
313            .eth
314            .logs(FilterBuilder::default().topic_filter(filter).build())
315            .await?;
316        logs.into_iter()
317            .map(move |l| {
318                let log = ev.parse_log(ethabi::RawLog {
319                    topics: l.topics,
320                    data: l.data.0,
321                })?;
322
323                R::from_tokens(log.params.into_iter().map(|x| x.value).collect::<Vec<_>>())
324            })
325            .collect::<Result<Vec<R>>>()
326    }
327}
328
329// #[cfg(feature = "signing")]
330mod contract_signing {
331    use super::*;
332    use crate::{
333        api::Accounts,
334        types::{SignedTransaction, TransactionParameters},
335    };
336
337    impl<T: Transport> Contract<T> {
338        pub async fn sign(
339            &self,
340            func: &str,
341            params: impl Tokenize,
342            options: Options,
343            from: String,
344            key_info: KeyInfo,
345            chain_id: u64,
346        ) -> crate::Result<SignedTransaction> {
347            let fn_data = self
348                .abi
349                .function(func)
350                .and_then(|function| function.encode_input(&params.into_tokens()))
351                // TODO [ToDr] SendTransactionWithConfirmation should support custom error type (so that we can return
352                // `contract::Error` instead of more generic `Error`.
353                .map_err(|err| crate::error::Error::Decoder(format!("{:?}", err)))?;
354            let accounts = Accounts::new(self.eth.transport().clone());
355            let mut tx = TransactionParameters {
356                nonce: options.nonce,
357                to: Some(self.address),
358                gas_price: options.gas_price,
359                data: Bytes(fn_data),
360                transaction_type: options.transaction_type,
361                access_list: options.access_list,
362                max_fee_per_gas: options.max_fee_per_gas,
363                max_priority_fee_per_gas: options.max_priority_fee_per_gas,
364                ..Default::default()
365            };
366            if let Some(gas) = options.gas {
367                tx.gas = gas;
368            }
369            if let Some(value) = options.value {
370                tx.value = value;
371            }
372            accounts.sign_transaction(tx, from, key_info, chain_id).await
373        }
374
375        /// Submit contract call transaction to the transaction pool.
376        ///
377        /// Note this function DOES NOT wait for any confirmations, so there is no guarantees that the call is actually executed.
378        /// If you'd rather wait for block inclusion, please use [`signed_call_with_confirmations`] instead.
379        pub async fn signed_call(
380            &self,
381            func: &str,
382            params: impl Tokenize,
383            options: Options,
384            from: String,
385            key_info: KeyInfo,
386            chain_id: u64,
387        ) -> crate::Result<H256> {
388            let signed = self.sign(func, params, options, from, key_info, chain_id).await?;
389            self.eth.send_raw_transaction(signed.raw_transaction).await
390        }
391
392        // Submit contract call transaction to the transaction pool and wait for the transaction to be included in a block.
393        //
394        // This function will wait for block inclusion of the transaction before returning.
395        // If you'd rather just submit transaction and receive it's hash, please use [`signed_call`] instead.
396        pub async fn signed_call_with_confirmations(
397            &self,
398            func: &str,
399            params: impl Tokenize,
400            options: Options,
401            from: String,
402            confirmations: usize,
403            key_info: KeyInfo,
404            chain_id: u64,
405        ) -> crate::Result<TransactionReceipt> {
406            let poll_interval = time::Duration::from_secs(1);
407            let signed = self.sign(func, params, options, from, key_info, chain_id).await?;
408
409            confirm::send_raw_transaction_with_confirmation(
410                self.eth.transport().clone(),
411                signed.raw_transaction,
412                poll_interval,
413                confirmations,
414            )
415            .await
416        }
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::{Contract, Options};
423    use crate::{
424        api::{self, Namespace},
425        rpc,
426        transports::test::TestTransport,
427        types::{Address, BlockId, BlockNumber, H256, U256},
428        Transport,
429    };
430
431    fn contract<T: Transport>(transport: &T) -> Contract<&T> {
432        let eth = api::Eth::new(transport);
433        Contract::from_json(eth, Address::from_low_u64_be(1), include_bytes!("./res/token.json")).unwrap()
434    }
435
436    #[test]
437    fn should_call_constant_function() {
438        // given
439        let mut transport = TestTransport::default();
440        transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
441
442        let result: String = {
443            let token = contract(&transport);
444
445            // when
446            futures::executor::block_on(token.query(
447                "name",
448                (),
449                None,
450                Options::default(),
451                BlockId::Number(BlockNumber::Number(1.into())),
452            ))
453            .unwrap()
454        };
455
456        // then
457        transport.assert_request(
458            "eth_call",
459            &[
460                "{\"data\":\"0x06fdde03\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
461                "\"0x1\"".into(),
462            ],
463        );
464        transport.assert_no_more_requests();
465        assert_eq!(result, "Hello World!".to_owned());
466    }
467
468    #[test]
469    fn should_call_constant_function_by_hash() {
470        // given
471        let mut transport = TestTransport::default();
472        transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
473
474        let result: String = {
475            let token = contract(&transport);
476
477            // when
478            futures::executor::block_on(token.query(
479                "name",
480                (),
481                None,
482                Options::default(),
483                BlockId::Hash(H256::default()),
484            ))
485            .unwrap()
486        };
487
488        // then
489        transport.assert_request(
490            "eth_call",
491            &[
492                "{\"data\":\"0x06fdde03\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(),
493                "{\"blockHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}".into(),
494            ],
495        );
496        transport.assert_no_more_requests();
497        assert_eq!(result, "Hello World!".to_owned());
498    }
499
500    #[test]
501    fn should_query_with_params() {
502        // given
503        let mut transport = TestTransport::default();
504        transport.set_response(rpc::Value::String("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000".into()));
505
506        let result: String = {
507            let token = contract(&transport);
508
509            // when
510            futures::executor::block_on(token.query(
511                "name",
512                (),
513                Address::from_low_u64_be(5),
514                Options::with(|options| {
515                    options.gas_price = Some(10_000_000.into());
516                }),
517                BlockId::Number(BlockNumber::Latest),
518            ))
519            .unwrap()
520        };
521
522        // then
523        transport.assert_request("eth_call", &["{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"gasPrice\":\"0x989680\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(), "\"latest\"".into()]);
524        transport.assert_no_more_requests();
525        assert_eq!(result, "Hello World!".to_owned());
526    }
527
528    #[test]
529    fn should_call_a_contract_function() {
530        // given
531        let mut transport = TestTransport::default();
532        transport.set_response(rpc::Value::String(format!("{:?}", H256::from_low_u64_be(5))));
533
534        let result = {
535            let token = contract(&transport);
536
537            // when
538            futures::executor::block_on(token.call("name", (), Address::from_low_u64_be(5), Options::default()))
539                .unwrap()
540        };
541
542        // then
543        transport.assert_request("eth_sendTransaction", &["{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into()]);
544        transport.assert_no_more_requests();
545        assert_eq!(result, H256::from_low_u64_be(5));
546    }
547
548    #[test]
549    fn should_estimate_gas_usage() {
550        // given
551        let mut transport = TestTransport::default();
552        transport.set_response(rpc::Value::String(format!("{:#x}", U256::from(5))));
553
554        let result = {
555            let token = contract(&transport);
556
557            // when
558            futures::executor::block_on(token.estimate_gas("name", (), Address::from_low_u64_be(5), Options::default()))
559                .unwrap()
560        };
561
562        // then
563        transport.assert_request("eth_estimateGas", &["{\"data\":\"0x06fdde03\",\"from\":\"0x0000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into()]);
564        transport.assert_no_more_requests();
565        assert_eq!(result, 5.into());
566    }
567
568    #[test]
569    fn should_query_single_parameter_function() {
570        // given
571        let mut transport = TestTransport::default();
572        transport.set_response(rpc::Value::String(
573            "0x0000000000000000000000000000000000000000000000000000000000000020".into(),
574        ));
575
576        let result: U256 = {
577            let token = contract(&transport);
578
579            // when
580            futures::executor::block_on(token.query(
581                "balanceOf",
582                Address::from_low_u64_be(5),
583                None,
584                Options::default(),
585                None,
586            ))
587            .unwrap()
588        };
589
590        // then
591        transport.assert_request("eth_call", &["{\"data\":\"0x70a082310000000000000000000000000000000000000000000000000000000000000005\",\"to\":\"0x0000000000000000000000000000000000000001\"}".into(), "\"latest\"".into()]);
592        transport.assert_no_more_requests();
593        assert_eq!(result, 0x20.into());
594    }
595}