ethcontract/contract/
method.rs

1//! Implementation for a contract method builder and call future. This is not
2//! intended to be used directly but to be used by a contract `Instance` with
3//! [Instance::method](ethcontract::contract::Instance::method).
4
5use crate::transaction::{Account, GasPrice, TransactionBuilder, TransactionResult};
6use crate::{batch::CallBatch, errors::MethodError, tokens::Tokenize};
7use ethcontract_common::abi::{Function, Token};
8use std::marker::PhantomData;
9use web3::types::{AccessList, Address, BlockId, Bytes, CallRequest, U256};
10use web3::Transport;
11use web3::{api::Web3, BatchTransport};
12
13/// Default options to be applied to `MethodBuilder` or `ViewMethodBuilder`.
14#[derive(Clone, Debug, Default)]
15pub struct MethodDefaults {
16    /// Default sender of the transaction with the signing strategy to use.
17    pub from: Option<Account>,
18    /// Default gas amount to use for transaction.
19    pub gas: Option<U256>,
20    /// Default gas price to use for transaction.
21    pub gas_price: Option<GasPrice>,
22}
23
24/// Data used for building a contract method call or transaction. The method
25/// builder can be demoted into a `CallBuilder` to not allow sending of
26/// transactions. This is useful when dealing with view functions.
27#[derive(Debug, Clone)]
28#[must_use = "methods do nothing unless you `.call()` or `.send()` them"]
29pub struct MethodBuilder<T: Transport, R: Tokenize> {
30    web3: Web3<T>,
31    function: Function,
32    /// transaction parameters
33    pub tx: TransactionBuilder<T>,
34    _result: PhantomData<R>,
35}
36
37impl<T: Transport> MethodBuilder<T, ()> {
38    /// Creates a new builder for a transaction invoking the fallback method.
39    pub fn fallback(web3: Web3<T>, address: Address, data: Bytes) -> Self {
40        // NOTE: We create a fake `Function` entry for the fallback method. This
41        //   is OK since it is only ever used for error formatting purposes.
42
43        #[allow(deprecated)]
44        let function = Function {
45            name: "fallback".into(),
46            inputs: vec![],
47            outputs: vec![],
48            constant: None,
49            state_mutability: Default::default(),
50        };
51        MethodBuilder::new(web3, function, address, data)
52    }
53}
54
55impl<T: Transport, R: Tokenize> MethodBuilder<T, R> {
56    /// Creates a new builder for a transaction.
57    pub fn new(web3: Web3<T>, function: Function, address: Address, data: Bytes) -> Self {
58        MethodBuilder {
59            web3: web3.clone(),
60            function,
61            tx: TransactionBuilder::new(web3).to(address).data(data),
62            _result: PhantomData,
63        }
64    }
65
66    /// Apply method defaults to this builder.
67    pub fn with_defaults(mut self, defaults: &MethodDefaults) -> Self {
68        self.tx.from = self.tx.from.or_else(|| defaults.from.clone());
69        self.tx.gas = self.tx.gas.or(defaults.gas);
70        self.tx.gas_price = self.tx.gas_price.or(defaults.gas_price);
71        self
72    }
73
74    /// Returns a reference to the underling ABI function for this call.
75    pub fn function(&self) -> &Function {
76        &self.function
77    }
78
79    /// Specify the signing method to use for the transaction, if not specified
80    /// the the transaction will be locally signed with the default user.
81    pub fn from(mut self, value: Account) -> Self {
82        self.tx = self.tx.from(value);
83        self
84    }
85
86    /// Secify amount of gas to use, if not specified then a gas estimate will
87    /// be used.
88    pub fn gas(mut self, value: U256) -> Self {
89        self.tx = self.tx.gas(value);
90        self
91    }
92
93    /// Specify the gas price to use, if not specified then the estimated gas
94    /// price will be used.
95    pub fn gas_price(mut self, value: GasPrice) -> Self {
96        self.tx = self.tx.gas_price(value);
97        self
98    }
99
100    /// Specify what how much ETH to transfer with the transaction, if not
101    /// specified then no ETH will be sent.
102    pub fn value(mut self, value: U256) -> Self {
103        self.tx = self.tx.value(value);
104        self
105    }
106
107    /// Specify the nonce for the transation, if not specified will use the
108    /// current transaction count for the signing account.
109    pub fn nonce(mut self, value: U256) -> Self {
110        self.tx = self.tx.nonce(value);
111        self
112    }
113
114    /// Specify the number of confirmations to wait for when confirming the
115    /// transaction, if not specified will wait for the transaction to be mined
116    /// without any extra confirmations.
117    pub fn confirmations(mut self, value: usize) -> Self {
118        self.tx = self.tx.confirmations(value);
119        self
120    }
121
122    /// Specify the access list for the transaction, if not specified no access list will be used.
123    pub fn access_list(mut self, value: AccessList) -> Self {
124        self.tx = self.tx.access_list(value);
125        self
126    }
127
128    /// Extract inner `TransactionBuilder` from this `SendBuilder`. This exposes
129    /// `TransactionBuilder` only APIs.
130    pub fn into_inner(self) -> TransactionBuilder<T> {
131        self.tx
132    }
133
134    /// Sign (if required) and send the method call transaction.
135    pub async fn send(self) -> Result<TransactionResult, MethodError> {
136        let Self { function, tx, .. } = self;
137        tx.send()
138            .await
139            .map_err(|err| MethodError::new(&function, err))
140    }
141
142    /// Demotes a `MethodBuilder` into a `ViewMethodBuilder` which has a more
143    /// restricted API and cannot actually send transactions.
144    pub fn view(self) -> ViewMethodBuilder<T, R> {
145        ViewMethodBuilder::from_method(self)
146    }
147
148    /// Call a contract method. Contract calls do not modify the blockchain and
149    /// as such do not require gas or signing. Note that doing a call with a
150    /// block number requires first demoting the `MethodBuilder` into a
151    /// `ViewMethodBuilder` and setting the block number for the call.
152    pub async fn call(self) -> Result<R, MethodError> {
153        self.view().call().await
154    }
155}
156
157/// Data used for building a contract method call. The view method builder can't
158/// directly send transactions and is for read only method calls.
159#[derive(Debug, Clone)]
160#[must_use = "view methods do nothing unless you `.call()` them"]
161pub struct ViewMethodBuilder<T: Transport, R: Tokenize> {
162    /// method parameters
163    pub m: MethodBuilder<T, R>,
164    /// optional block number
165    pub block: Option<BlockId>,
166}
167
168impl<T: Transport, R: Tokenize> ViewMethodBuilder<T, R> {
169    /// Create a new `ViewMethodBuilder` by demoting a `MethodBuilder`.
170    pub fn from_method(method: MethodBuilder<T, R>) -> Self {
171        ViewMethodBuilder {
172            m: method,
173            block: None,
174        }
175    }
176
177    /// Apply method defaults to this builder.
178    pub fn with_defaults(mut self, defaults: &MethodDefaults) -> Self {
179        self.m = self.m.with_defaults(defaults);
180        self
181    }
182
183    /// Returns a reference to the underling ABI function for this call.
184    pub fn function(&self) -> &Function {
185        &self.m.function
186    }
187
188    /// Specify the account the transaction is being sent from.
189    pub fn from(mut self, value: Address) -> Self {
190        self.m = self.m.from(Account::Local(value, None));
191        self
192    }
193
194    /// Secify amount of gas to use, if not specified then a gas estimate will
195    /// be used.
196    pub fn gas(mut self, value: U256) -> Self {
197        self.m = self.m.gas(value);
198        self
199    }
200
201    /// Specify the gas price to use, if not specified then the estimated gas
202    /// price will be used.
203    pub fn gas_price(mut self, value: GasPrice) -> Self {
204        self.m = self.m.gas_price(value);
205        self
206    }
207
208    /// Specify what how much ETH to transfer with the transaction, if not
209    /// specified then no ETH will be sent.
210    pub fn value(mut self, value: U256) -> Self {
211        self.m = self.m.value(value);
212        self
213    }
214
215    /// Specify the nonce for the transation, if not specified will use the
216    /// current transaction count for the signing account.
217    pub fn nonce(mut self, value: U256) -> Self {
218        self.m = self.m.nonce(value);
219        self
220    }
221
222    /// Specify the access list for the transaction, if not specified no access list will be used.
223    pub fn access_list(mut self, value: AccessList) -> Self {
224        self.m = self.m.access_list(value);
225        self
226    }
227
228    /// Specify the block height for the call, if not specified then latest
229    /// mined block will be used.
230    pub fn block(mut self, value: BlockId) -> Self {
231        self.block = Some(value);
232        self
233    }
234}
235
236impl<T: Transport, R: Tokenize> ViewMethodBuilder<T, R> {
237    /// Call a contract method. Contract calls do not modify the blockchain and
238    /// as such do not require gas or signing.
239    pub async fn call(self) -> Result<R, MethodError> {
240        let eth = &self.m.web3.eth();
241        let (function, call, block) = self.decompose();
242        let future = eth.call(call, block);
243        convert_response::<_, R>(future, function).await
244    }
245
246    /// Adds this view method to a batch. Allows execution with other contract calls in one roundtrip
247    /// The returned future only resolve once `batch` is resolved. Panics, if `batch` is dropped before
248    /// executing
249    pub fn batch_call<B: BatchTransport>(
250        self,
251        batch: &mut CallBatch<B>,
252    ) -> impl std::future::Future<Output = Result<R, MethodError>> {
253        let (function, call, block) = self.decompose();
254        let future = batch.push(call, block);
255        async move { convert_response::<_, R>(future, function).await }
256    }
257
258    fn decompose(self) -> (Function, CallRequest, Option<BlockId>) {
259        let resolved_gas_price = self
260            .m
261            .tx
262            .gas_price
263            .map(|gas_price| gas_price.resolve_for_transaction())
264            .unwrap_or_default();
265        (
266            self.m.function,
267            CallRequest {
268                from: self.m.tx.from.map(|account| account.address()),
269                to: self.m.tx.to,
270                gas: self.m.tx.gas,
271                gas_price: resolved_gas_price.gas_price,
272                value: self.m.tx.value,
273                data: self.m.tx.data,
274                transaction_type: resolved_gas_price.transaction_type,
275                access_list: self.m.tx.access_list,
276                max_fee_per_gas: resolved_gas_price.max_fee_per_gas,
277                max_priority_fee_per_gas: resolved_gas_price.max_priority_fee_per_gas,
278            },
279            self.block,
280        )
281    }
282}
283
284async fn convert_response<
285    F: std::future::Future<Output = Result<Bytes, web3::Error>>,
286    R: Tokenize,
287>(
288    future: F,
289    function: Function,
290) -> Result<R, MethodError> {
291    let bytes = future
292        .await
293        .map_err(|err| MethodError::new(&function, err))?;
294    let tokens = function
295        .decode_output(&bytes.0)
296        .map_err(|err| MethodError::new(&function, err))?;
297    let token = match tokens.len() {
298        0 => Token::Tuple(Vec::new()),
299        1 => tokens.into_iter().next().unwrap(),
300        // Older versions of solc emit a list of tokens as the return type of functions returning
301        // tuples instead of a single type that is a tuple. In order to be backwards compatible we
302        // accept this too.
303        _ => Token::Tuple(tokens),
304    };
305    let result = R::from_token(token).map_err(|err| MethodError::new(&function, err))?;
306    Ok(result)
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use crate::test::prelude::*;
313    use ethcontract_common::abi::{Param, ParamType};
314    use web3::types::AccessListItem;
315
316    fn test_abi_function() -> (Function, Bytes) {
317        #[allow(deprecated)]
318        let function = Function {
319            name: "test".to_owned(),
320            inputs: Vec::new(),
321            outputs: vec![Param {
322                name: "".to_owned(),
323                kind: ParamType::Uint(256),
324                internal_type: None,
325            }],
326            constant: None,
327            state_mutability: Default::default(),
328        };
329        let data = function
330            .encode_input(&[])
331            .expect("error encoding empty input");
332
333        (function, Bytes(data))
334    }
335
336    #[test]
337    fn method_tx_options() {
338        let transport = TestTransport::new();
339        let web3 = Web3::new(transport.clone());
340
341        let address = addr!("0x0123456789012345678901234567890123456789");
342        let from = addr!("0x9876543210987654321098765432109876543210");
343        let (function, data) = test_abi_function();
344        let tx = MethodBuilder::<_, U256>::new(web3, function, address, data.clone())
345            .from(Account::Local(from, None))
346            .gas(1.into())
347            .gas_price(2.0.into())
348            .value(28.into())
349            .nonce(42.into())
350            .access_list(vec![AccessListItem::default()])
351            .into_inner();
352
353        assert_eq!(tx.from.map(|a| a.address()), Some(from));
354        assert_eq!(tx.to, Some(address));
355        assert_eq!(tx.gas, Some(1.into()));
356        assert_eq!(tx.gas_price, Some(2.0.into()));
357        assert_eq!(tx.value, Some(28.into()));
358        assert_eq!(tx.data, Some(data));
359        assert_eq!(tx.nonce, Some(42.into()));
360        assert_eq!(tx.access_list, Some(vec![AccessListItem::default()]));
361        transport.assert_no_more_requests();
362    }
363
364    #[test]
365    fn view_method_call() {
366        let mut transport = TestTransport::new();
367        let web3 = Web3::new(transport.clone());
368
369        let address = addr!("0x0123456789012345678901234567890123456789");
370        let from = addr!("0x9876543210987654321098765432109876543210");
371        let (function, data) = test_abi_function();
372        let tx = ViewMethodBuilder::<_, U256>::from_method(MethodBuilder::new(
373            web3,
374            function,
375            address,
376            data.clone(),
377        ))
378        .from(from)
379        .gas(1.into())
380        .gas_price(2.0.into())
381        .value(28.into())
382        .block(BlockId::Number(100.into()));
383
384        transport.add_response(json!(
385            "0x000000000000000000000000000000000000000000000000000000000000002a"
386        )); // call response
387        let result = tx.call().immediate().expect("call error");
388
389        assert_eq!(result, 42.into());
390        transport.assert_request(
391            "eth_call",
392            &[
393                json!({
394                    "from": from,
395                    "to": address,
396                    "gas": "0x1",
397                    "gasPrice": "0x2",
398                    "value": "0x1c",
399                    "data": data,
400                }),
401                json!("0x64"),
402            ],
403        );
404        transport.assert_no_more_requests();
405    }
406
407    #[test]
408    fn method_to_view_method_preserves_options() {
409        let mut transport = TestTransport::new();
410        let web3 = Web3::new(transport.clone());
411
412        let address = addr!("0x0123456789012345678901234567890123456789");
413        let (function, data) = test_abi_function();
414        let tx = MethodBuilder::<_, U256>::new(web3, function, address, data.clone())
415            .gas(42.into())
416            .view();
417
418        transport.add_response(json!(
419            "0x0000000000000000000000000000000000000000000000000000000000000000"
420        ));
421        tx.call().immediate().expect("call error");
422
423        transport.assert_request(
424            "eth_call",
425            &[
426                json!({
427                    "to": address,
428                    "gas": "0x2a",
429                    "data": data,
430                }),
431                json!("latest"),
432            ],
433        );
434        transport.assert_no_more_requests();
435    }
436
437    #[test]
438    fn method_defaults_are_applied() {
439        let transport = TestTransport::new();
440        let web3 = Web3::new(transport.clone());
441
442        let from = addr!("0x9876543210987654321098765432109876543210");
443        let address = addr!("0x0123456789012345678901234567890123456789");
444        let (function, data) = test_abi_function();
445        let tx = MethodBuilder::<_, U256>::new(web3, function, address, data)
446            .with_defaults(&MethodDefaults {
447                from: Some(Account::Local(from, None)),
448                gas: Some(1.into()),
449                gas_price: Some(2.0.into()),
450            })
451            .into_inner();
452
453        assert_eq!(tx.from.map(|a| a.address()), Some(from));
454        assert_eq!(tx.gas, Some(1.into()));
455        assert_eq!(tx.gas_price, Some(2.0.into()));
456        transport.assert_no_more_requests();
457    }
458}