Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block identifier #596

Merged
merged 4 commits into from
Jun 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions brownie/network/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import warnings
from pathlib import Path
from textwrap import TextWrapper
from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Union
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
from urllib.parse import urlparse

import eth_abi
Expand Down Expand Up @@ -793,7 +793,7 @@ def __call__(self, *args: Tuple) -> Any:
fn = self._get_fn_from_args(args)
return fn(*args) # type: ignore

def call(self, *args: Tuple) -> Any:
def call(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) -> Any:
"""
Call the contract method without broadcasting a transaction.

Expand All @@ -803,16 +803,20 @@ def call(self, *args: Tuple) -> Any:

Arguments
---------
*args:
*args
Contract method inputs. You can optionally provide a
dictionary of transaction properties as the last arg.
block_identifier : int | str | bytes, optional
A block number or hash that the call is executed at. If not given, the
latest block used. Raises `ValueError` if this value is too far in the
past and you are not using an archival node.

Returns
-------
Contract method return value(s).
"""
fn = self._get_fn_from_args(args)
return fn.call(*args)
return fn.call(*args, block_identifier=block_identifier)

def transact(self, *args: Tuple) -> TransactionReceiptType:
"""
Expand Down Expand Up @@ -894,15 +898,19 @@ def info(self) -> None:
print(f"{self.abi['name']}({_inputs(self.abi)})")
_print_natspec(self.natspec)

def call(self, *args: Tuple) -> Any:
def call(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) -> Any:
"""
Call the contract method without broadcasting a transaction.

Arguments
---------
*args:
*args
Contract method inputs. You can optionally provide a
dictionary of transaction properties as the last arg.
block_identifier : int | str | bytes, optional
A block number or hash that the call is executed at. If not given, the
latest block used. Raises `ValueError` if this value is too far in the
past and you are not using an archival node.

Returns
-------
Expand All @@ -914,9 +922,10 @@ def call(self, *args: Tuple) -> Any:
tx["from"] = str(tx["from"])
tx.update({"to": self._address, "data": self.encode_input(*args)})
try:
data = web3.eth.call(dict((k, v) for k, v in tx.items() if v))
data = web3.eth.call({k: v for k, v in tx.items() if v}, block_identifier)
except ValueError as e:
raise VirtualMachineError(e) from None

return self.decode_output(data)

def transact(self, *args: Tuple) -> TransactionReceiptType:
Expand All @@ -925,7 +934,7 @@ def transact(self, *args: Tuple) -> TransactionReceiptType:

Arguments
---------
*args:
*args
Contract method inputs. You can optionally provide a
dictionary of transaction properties as the last arg.

Expand Down Expand Up @@ -1003,7 +1012,7 @@ def __call__(self, *args: Tuple) -> TransactionReceiptType:

Arguments
---------
*args:
*args
Contract method inputs. You can optionally provide a
dictionary of transaction properties as the last arg.

Expand All @@ -1029,23 +1038,28 @@ class ContractCall(_ContractMethod):
Bytes4 method signature.
"""

def __call__(self, *args: Tuple) -> Callable:
def __call__(self, *args: Tuple, block_identifier: Union[int, str, bytes] = None) -> Any:
"""
Call the contract method without broadcasting a transaction.

Arguments
---------
*args:
args
Contract method inputs. You can optionally provide a
dictionary of transaction properties as the last arg.
block_identifier : int | str | bytes, optional
A block number or hash that the call is executed at. If not given, the
latest block used. Raises `ValueError` if this value is too far in the
past and you are not using an archival node.

Returns
-------
Contract method return value(s).
"""

if not CONFIG.argv["always_transact"]:
return self.call(*args)
if not CONFIG.argv["always_transact"] or block_identifier is not None:
return self.call(*args, block_identifier=block_identifier)

args, tx = _get_tx(self._owner, args)
tx.update({"gas_price": 0, "from": self._owner or accounts[0]})
try:
Expand Down
10 changes: 7 additions & 3 deletions docs/api-network.rst
Original file line number Diff line number Diff line change
Expand Up @@ -930,11 +930,12 @@ Contract Internal Attributes
ContractCall
------------

.. py:class:: brownie.network.contract.ContractCall(*args)
.. py:class:: brownie.network.contract.ContractCall(*args, block_identifier=None)

Calls a non state-changing contract method without broadcasting a transaction, and returns the result. ``args`` must match the required inputs for the method.

The expected inputs are shown in the method's ``__repr__`` value.
* ``args``: Input arguments for the call. The expected inputs are shown in the method's ``__repr__`` value.
* ``block_identifier``: A block number or hash that the call is executed at. If ``None``, the latest block is used. Raises `ValueError` if this value is too far in the past and you are not using an archival node.

Inputs and return values are formatted via methods in the :ref:`convert<api-convert>` module. Multiple values are returned inside a :func:`ReturnValue <brownie.convert.datatypes.ReturnValue>`.

Expand Down Expand Up @@ -1061,10 +1062,13 @@ ContractTx Attributes
ContractTx Methods
******************

.. py:classmethod:: ContractTx.call(*args)
.. py:classmethod:: ContractTx.call(*args, block_identifier=None)

Calls the contract method without broadcasting a transaction, and returns the result.

* ``args``: Input arguments for the call. The expected inputs are shown in the method's ``__repr__`` value.
* ``block_identifier``: A block number or hash that the call is executed at. If ``None``, the latest block is used. Raises `ValueError` if this value is too far in the past and you are not using an archival node.

Inputs and return values are formatted via methods in the :ref:`convert<api-convert>` module. Multiple values are returned inside a :func:`ReturnValue <brownie.convert.datatypes.ReturnValue>`.

.. code-block:: python
Expand Down
37 changes: 37 additions & 0 deletions tests/network/contract/test_contractcall.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/python3
import pytest

from brownie import compile_source


def test_attributes(accounts, tester):
assert tester.getTuple._address == tester.address
Expand All @@ -22,21 +24,56 @@ def test_transact(accounts, tester):
assert accounts[0].nonce == nonce + 1


def test_block_identifier(accounts, history):
contract = compile_source(
"""
foo: public(int128)

@public
def set_foo(_foo: int128):
self.foo = _foo
"""
).Vyper.deploy({"from": accounts[0]})

contract.set_foo(13)
contract.set_foo(42)

assert contract.foo() == 42
assert contract.foo(block_identifier=history[-2].block_number) == 13
assert contract.foo.call(block_identifier=history[-2].block_number) == 13


def test_always_transact(accounts, tester, argv, web3, monkeypatch, history):
owner = tester.owner()
argv["always_transact"] = True
height = web3.eth.blockNumber
result = tester.owner()

assert owner == result
assert web3.eth.blockNumber == height == len(history)

monkeypatch.setattr("brownie.network.rpc.undo", lambda: None)
result = tester.owner()
tx = history[-1]

assert owner == result
assert web3.eth.blockNumber == height + 1 == len(history)
assert tx.fn_name == "owner"


def test_always_transact_block_identifier(accounts, tester, argv, web3, monkeypatch, history):
argv["always_transact"] = True
height = web3.eth.blockNumber
last_tx = history[-1]

monkeypatch.setattr("brownie.network.rpc.undo", lambda: None)
tester.owner(block_identifier="latest")

# using `block_identifier` should take priority over `always_transact`
assert web3.eth.blockNumber == height
assert last_tx == history[-1]


def test_nonce_manual_call(tester, accounts):
"""manual nonce is ignored when calling without transact"""
nonce = accounts[0].nonce
Expand Down