pink_web3/contract/
mod.rs

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