mwc_web3/contract/
mod.rs

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