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