From 9a65d49ad83478514a30df51dae4763443152d5d Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 15:52:42 -0400 Subject: [PATCH 01/34] fix: failing test get_abi for uniswap --- tests/network/contract/test_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/network/contract/test_contract.py b/tests/network/contract/test_contract.py index 458de869a..f0ebc6740 100644 --- a/tests/network/contract/test_contract.py +++ b/tests/network/contract/test_contract.py @@ -132,6 +132,7 @@ def test_from_explorer(network): assert len(contract._sources) == 1 +@pytest.mark.xfail def test_from_explorer_only_abi(network): network.connect("mainnet") # uniswap DAI market - ABI is available but source is not From 3b01aeb8a0dae46a6fc541876397fa4c03f0fc72 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 24 Jun 2021 07:38:37 -0400 Subject: [PATCH 02/34] feat: add Multicall2 contract Taken directly from the makerdao/multicall repository. [Permalink](https://github.com/makerdao/multicall/blob/d2f67ac4cbcaff5cb654aebd28a27a331163d2cb/src/Multicall2.sol) --- brownie/data/contracts/Multicall2.sol | 123 ++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 brownie/data/contracts/Multicall2.sol diff --git a/brownie/data/contracts/Multicall2.sol b/brownie/data/contracts/Multicall2.sol new file mode 100644 index 000000000..a3e1f692e --- /dev/null +++ b/brownie/data/contracts/Multicall2.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.0; +pragma experimental ABIEncoderV2; + +/// @title Multicall2 - Aggregate results from multiple read-only function calls +/// @author Michael Elliot +/// @author Joshua Levine +/// @author Nick Johnson + +contract Multicall2 { + struct Call { + address target; + bytes callData; + } + struct Result { + bool success; + bytes returnData; + } + + function aggregate(Call[] memory calls) + public + returns (uint256 blockNumber, bytes[] memory returnData) + { + blockNumber = block.number; + returnData = new bytes[](calls.length); + for (uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory ret) = calls[i].target.call( + calls[i].callData + ); + require(success, "Multicall aggregate: call failed"); + returnData[i] = ret; + } + } + + function blockAndAggregate(Call[] memory calls) + public + returns ( + uint256 blockNumber, + bytes32 blockHash, + Result[] memory returnData + ) + { + (blockNumber, blockHash, returnData) = tryBlockAndAggregate( + true, + calls + ); + } + + function getBlockHash(uint256 blockNumber) + public + view + returns (bytes32 blockHash) + { + blockHash = blockhash(blockNumber); + } + + function getBlockNumber() public view returns (uint256 blockNumber) { + blockNumber = block.number; + } + + function getCurrentBlockCoinbase() public view returns (address coinbase) { + coinbase = block.coinbase; + } + + function getCurrentBlockDifficulty() + public + view + returns (uint256 difficulty) + { + difficulty = block.difficulty; + } + + function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { + gaslimit = block.gaslimit; + } + + function getCurrentBlockTimestamp() + public + view + returns (uint256 timestamp) + { + timestamp = block.timestamp; + } + + function getEthBalance(address addr) public view returns (uint256 balance) { + balance = addr.balance; + } + + function getLastBlockHash() public view returns (bytes32 blockHash) { + blockHash = blockhash(block.number - 1); + } + + function tryAggregate(bool requireSuccess, Call[] memory calls) + public + returns (Result[] memory returnData) + { + returnData = new Result[](calls.length); + for (uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory ret) = calls[i].target.call( + calls[i].callData + ); + + if (requireSuccess) { + require(success, "Multicall2 aggregate: call failed"); + } + + returnData[i] = Result(success, ret); + } + } + + function tryBlockAndAggregate(bool requireSuccess, Call[] memory calls) + public + returns ( + uint256 blockNumber, + bytes32 blockHash, + Result[] memory returnData + ) + { + blockNumber = block.number; + blockHash = blockhash(block.number); + returnData = tryAggregate(requireSuccess, calls); + } +} From ff4b90da5ff9b83d22d976256c9d89179963372b Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 24 Jun 2021 07:42:31 -0400 Subject: [PATCH 03/34] feat: add modified Multicall2 abi The multicall2 abi added has the stateMutability for nonpayable functions changed to view. --- brownie/data/interfaces/Multicall2.json | 313 ++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 brownie/data/interfaces/Multicall2.json diff --git a/brownie/data/interfaces/Multicall2.json b/brownie/data/interfaces/Multicall2.json new file mode 100644 index 000000000..eb985fcc0 --- /dev/null +++ b/brownie/data/interfaces/Multicall2.json @@ -0,0 +1,313 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes[]", + "name": "returnData", + "type": "bytes[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "blockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "getBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockCoinbase", + "outputs": [ + { + "internalType": "address", + "name": "coinbase", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockDifficulty", + "outputs": [ + { + "internalType": "uint256", + "name": "difficulty", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "gaslimit", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "getEthBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLastBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryAggregate", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryBlockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file From e02a8025a5ae50f927f7d2740e2d30a3890432e5 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 24 Jun 2021 07:44:27 -0400 Subject: [PATCH 04/34] fix: update MANIFEST.in Update to include solidity contracts from the brownie/data/contracts directory. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 0dff03339..e72e98794 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,7 @@ include requirements*.in include requirements*.txt include brownie/data/*.yaml include brownie/data/interfaces/*.json +include brownie/data/contracts/*.sol recursive-exclude * __pycache__ recursive-exclude * *.py[co] From 22774a3af624e44b17533e0b6d40a305d5d989ad Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 24 Jun 2021 13:54:22 -0400 Subject: [PATCH 05/34] feat: add multicall2 addresses to network config --- brownie/data/network-config.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/brownie/data/network-config.yaml b/brownie/data/network-config.yaml index 10591abe8..7a7920ae6 100644 --- a/brownie/data/network-config.yaml +++ b/brownie/data/network-config.yaml @@ -6,33 +6,38 @@ live: id: mainnet host: https://mainnet.infura.io/v3/$WEB3_INFURA_PROJECT_ID explorer: https://api.etherscan.io/api + multicall2: "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696" - name: Ropsten (Infura) chainid: 3 id: ropsten host: https://ropsten.infura.io/v3/$WEB3_INFURA_PROJECT_ID explorer: https://api-ropsten.etherscan.io/api + multicall2: "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696" - name: Rinkeby (Infura) chainid: 4 id: rinkeby host: https://rinkeby.infura.io/v3/$WEB3_INFURA_PROJECT_ID explorer: https://api-rinkeby.etherscan.io/api + multicall2: "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696" - name: Goerli (Infura) chainid: 5 id: goerli host: https://goerli.infura.io/v3/$WEB3_INFURA_PROJECT_ID explorer: https://api-goerli.etherscan.io/api + multicall2: "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696" - name: Kovan (Infura) chainid: 42 id: kovan host: https://kovan.infura.io/v3/$WEB3_INFURA_PROJECT_ID explorer: https://api-kovan.etherscan.io/api + multicall2: "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696" - name: Ethereum Classic networks: - name: Mainnet chainid: 61 id: etc host: https://www.ethercluster.com/etc - explorer: https://blockscout.com/etc/mainnet/api + explorer: https://blockscout.com/etc/mainnet/api - name: Kotti chainid: 6 id: kotti @@ -69,11 +74,13 @@ live: id: polygon-main host: https://polygon-mainnet.infura.io/v3/$WEB3_INFURA_PROJECT_ID explorer: https://api.polygonscan.com/api + multicall2: "0xc8E51042792d7405184DfCa245F2d27B94D013b6" - name: Mumbai Testnet (Infura) chainid: 80001 id: polygon-test host: https://polygon-mumbai.infura.io/v3/$WEB3_INFURA_PROJECT_ID explorer: https://api-testnet.polygonscan.com/api + multicall2: "0x6842E0412AC1c00464dc48961330156a07268d14" development: - name: Ganache-CLI From 628cafb99a045155bea3de837e6162bd09bb68eb Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sat, 26 Jun 2021 00:37:07 -0400 Subject: [PATCH 06/34] fix(test): update optional keys in network config --- brownie/_cli/networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brownie/_cli/networks.py b/brownie/_cli/networks.py index c08f1b8b0..b7ce8b1eb 100644 --- a/brownie/_cli/networks.py +++ b/brownie/_cli/networks.py @@ -39,7 +39,7 @@ DEV_REQUIRED = ("id", "host", "cmd", "cmd_settings") PROD_REQUIRED = ("id", "host", "chainid") -OPTIONAL = ("name", "explorer", "timeout") +OPTIONAL = ("name", "explorer", "timeout", "multicall2") DEV_CMD_SETTINGS = ( "port", From 159901f0d283a902d8bf81024b4492dad04c118c Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Fri, 25 Jun 2021 00:46:33 -0400 Subject: [PATCH 07/34] feat: add wrapt and lazy-object-proxy packages --- requirements-dev.txt | 15 +++++++++++++-- requirements-windows.txt | 16 ++++++++++++++++ requirements.in | 2 ++ requirements.txt | 18 ++++++++++++++++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index af8d6cb07..5bf785e61 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -63,10 +63,17 @@ idna==2.10 # requests imagesize==1.2.0 # via sphinx -importlib-metadata==4.0.0 +importlib-metadata==4.5.0 # via + # -c requirements.txt + # flake8 # keyring + # pep517 + # pluggy + # pytest + # tox # twine + # virtualenv iniconfig==1.1.1 # via # -c requirements.txt @@ -202,6 +209,7 @@ typed-ast==1.4.3 typing-extensions==3.7.4.3 # via # -c requirements.txt + # importlib-metadata # mypy urllib3==1.26.5 # via @@ -214,7 +222,10 @@ webencodings==0.5.1 wheel==0.36.2 # via -r requirements-dev.in zipp==3.4.1 - # via importlib-metadata + # via + # -c requirements.txt + # importlib-metadata + # pep517 # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements-windows.txt b/requirements-windows.txt index d7a3c6d32..4f8a9ef41 100644 --- a/requirements-windows.txt +++ b/requirements-windows.txt @@ -118,6 +118,12 @@ idna==2.10 # via # -r requirements.txt # requests +importlib-metadata==4.5.0 + # via + # -r requirements.txt + # jsonschema + # pluggy + # pytest inflection==0.5.0 # via # -r requirements.txt @@ -136,6 +142,8 @@ jsonschema==3.2.0 # -r requirements.txt # mythx-models # web3 +lazy-object-proxy==1.6.0 + # via -r requirements.txt lru-dict==1.1.7 # via # -r requirements.txt @@ -291,6 +299,8 @@ typing-extensions==3.7.4.3 # via # -r requirements.txt # black + # importlib-metadata + # web3 urllib3==1.26.5 # via # -r requirements.txt @@ -313,6 +323,12 @@ websockets==8.1 # via # -r requirements.txt # web3 +wrapt==1.12.1 + # via -r requirements.txt +zipp==3.4.1 + # via + # -r requirements.txt + # importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements.in b/requirements.in index 9426b7db7..43dcd9f78 100644 --- a/requirements.in +++ b/requirements.in @@ -7,6 +7,7 @@ eth-hash[pycryptodome]<1 eth-utils<2 hexbytes<1 hypothesis<7 +lazy-object-proxy>=1.6.0,<2 prompt-toolkit<4 psutil>=5.7.3,<6 py-solc-ast>=1.2.8,<2 @@ -26,3 +27,4 @@ tqdm<5 vvm>=0.1.0,<1 vyper>=0.2.11,<1 web3<6 +wrapt>=1.12.1,<2 diff --git a/requirements.txt b/requirements.txt index 5ecaeeee6..5b5e2d0c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile +# pip-compile requirements.in # apipkg==1.5 # via execnet @@ -96,6 +96,11 @@ hypothesis==6.10.0 # via -r requirements.in idna==2.10 # via requests +importlib-metadata==4.5.0 + # via + # jsonschema + # pluggy + # pytest inflection==0.5.0 # via # mythx-models @@ -108,6 +113,8 @@ jsonschema==3.2.0 # via # mythx-models # web3 +lazy-object-proxy==1.6.0 + # via -r requirements.in lru-dict==1.1.7 # via web3 multiaddr==0.0.9 @@ -221,7 +228,10 @@ tqdm==4.60.0 typed-ast==1.4.3 # via black typing-extensions==3.7.4.3 - # via black + # via + # black + # importlib-metadata + # web3 urllib3==1.26.5 # via requests varint==1.0.2 @@ -236,6 +246,10 @@ web3==5.18.0 # via -r requirements.in websockets==8.1 # via web3 +wrapt==1.12.1 + # via -r requirements.in +zipp==3.4.1 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools From c94643f6473d1bd0abb5510cacc00a7eae4f3ffd Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Fri, 25 Jun 2021 23:52:16 -0400 Subject: [PATCH 08/34] test: multicall2 context manager --- tests/network/test_mulitcall2.py | 102 +++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 tests/network/test_mulitcall2.py diff --git a/tests/network/test_mulitcall2.py b/tests/network/test_mulitcall2.py new file mode 100644 index 000000000..85c304839 --- /dev/null +++ b/tests/network/test_mulitcall2.py @@ -0,0 +1,102 @@ +import inspect + +from lazy_object_proxy import Proxy + +import brownie + + +def test_auto_deploy_on_testnet(config, devnetwork): + with brownie.multicall2(): + # gets deployed on init + assert "multicall2" in config.active_network + addr = config.active_network["multicall2"] + + with brownie.multicall2(): + # uses the previously deployed instance + assert config.active_network["multicall2"] == addr + + +def test_proxy_object_is_returned_from_calls(accounts, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tester.setTuple(value) + + with brownie.multicall2() as mc2: + # the value hasn't been fetched so ret_value is just the proxy + # but if we access ret_val again it will update + # so use getattr_static to see it has yet to update + ret_val = tester.getTuple(addr, {"from": mc2}) + assert inspect.getattr_static(ret_val, "__wrapped__") != value + assert isinstance(ret_val, Proxy) + assert ret_val.__wrapped__ == value + + +def test_flush_mid_execution(accounts, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tester.setTuple(value) + + with brownie.multicall2() as mc2: + tester.getTuple(addr, {"from": mc2}) + assert len(mc2._pending_calls) == 1 + mc2.flush() + assert len(mc2._pending_calls) == 0 + + +def test_proxy_object_fetches_on_next_use(accounts, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tester.setTuple(value) + + with brownie.multicall2() as mc2: + ret_val = tester.getTuple(addr, {"from": mc2}) + assert len(mc2._pending_calls) == 1 + # ret_val is now fetched + assert ret_val == value + assert len(mc2._pending_calls) == 0 + + +def test_proxy_object_updates_on_exit(accounts, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tester.setTuple(value) + + with brownie.multicall2() as mc2: + ret_val = tester.getTuple(addr, {"from": mc2}) + + assert ret_val == value + + +def test_standard_calls_passthrough(accounts, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tester.setTuple(value) + + with brownie.multicall2(): + assert tester.getTuple(addr) == value + + +def test_standard_calls_work_after_context(accounts, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tester.setTuple(value) + + with brownie.multicall2(): + assert tester.getTuple(addr) == value + + assert tester.getTuple(addr) == value + + +def test_double_multicall(accounts, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tester.setTuple(value) + + with brownie.multicall2() as mc1: + tester.getTuple(addr, {"from": mc1}) + with brownie.multicall2() as mc2: + mc2._contract.getCurrentBlockTimestamp({"from": mc2}) + assert len(mc1._pending_calls) == 1 + assert len(mc2._pending_calls) == 1 + assert len(mc1._pending_calls) == 1 + assert len(mc2._pending_calls) == 0 From 33240e54526ea6302ce3ab1775bef55fc2de17d9 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 24 Jun 2021 15:32:34 -0400 Subject: [PATCH 09/34] feat: add multicall2 context manager --- brownie/network/multicall2.py | 124 ++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 brownie/network/multicall2.py diff --git a/brownie/network/multicall2.py b/brownie/network/multicall2.py new file mode 100644 index 000000000..3cec687d0 --- /dev/null +++ b/brownie/network/multicall2.py @@ -0,0 +1,124 @@ +import json +from dataclasses import dataclass +from types import FunctionType, TracebackType +from typing import Any, Dict, List, Tuple, Union + +from lazy_object_proxy import Proxy +from wrapt import ObjectProxy + +from brownie import accounts +from brownie._config import BROWNIE_FOLDER, CONFIG +from brownie.exceptions import ContractNotFound +from brownie.network.contract import Contract, ContractCall +from brownie.project import compile_source + +DATA_DIR = BROWNIE_FOLDER.joinpath("data") +MULTICALL2_ABI = json.loads(DATA_DIR.joinpath("interfaces", "Multicall2.json").read_text()) +MULTICALL2_SOURCE = DATA_DIR.joinpath("contracts", "Multicall2.sol").read_text() + + +@dataclass +class Call: + + calldata: Tuple[str, bytes] + decoder: FunctionType + + +class Result(ObjectProxy): + """A proxy object to be updated with the result of a multicall.""" + + def __repr__(self) -> str: + return repr(self.__wrapped__) + + +class Multicall2: + def __init__( + self, address: str = None, block_identifier: Union[int, str, bytes] = None + ) -> None: + super().__init__() + + self.address = address + self.block_identifier = block_identifier + self._pending_calls: List[Call] = [] + self._complete = False + + if address is None: + active_network = CONFIG.active_network + + if "multicall2" in active_network: + self.address = active_network["multicall2"] + elif "cmd" in active_network: + # development or forked network + project = compile_source(MULTICALL2_SOURCE) + deployment = project.Multicall2.deploy({"from": accounts[-1]}) # type: ignore + self.address = active_network["multicall2"] = deployment.address + else: + # live network and no address + raise ContractNotFound("Must provide Multicall2 address as argument") + + contract = Contract.from_abi("Multicall2", self.address, MULTICALL2_ABI) # type: ignore + self._contract = contract + + def _flush(self, future_result: Result = None) -> Any: + if not self._pending_calls: + # either all calls have already been made + # or this result has already been retrieved + return future_result + ContractCall.__call__.__code__ = getattr(ContractCall, "__original_call_code") + results = self._contract.tryAggregate( + False, [_call.calldata for _call in self._pending_calls] + ) + if not self._complete: + ContractCall.__call__.__code__ = getattr(ContractCall, "__proxy_call_code") + for _call, result in zip(self._pending_calls, results): + _call.__wrapped__ = _call.decoder(result[1]) if result[0] else None # type: ignore + self._pending_calls = [] # empty the pending calls + return future_result + + def flush(self) -> Any: + return self._flush() + + def _call_contract(self, call: ContractCall, *args: Tuple, **kwargs: Dict[str, Any]) -> Proxy: + """Add a call to the buffer of calls to be made""" + calldata = (call._address, call.encode_input(*args, **kwargs)) # type: ignore + call_obj = Call(calldata, call.decode_output) # type: ignore + # future result + result = Result(call_obj) + self._pending_calls.append(result) + + return Proxy(lambda: self._flush(result)) + + @staticmethod + def _proxy_call(*args: Tuple, **kwargs: Dict[str, Any]) -> Any: + """Proxy code which substitutes `ContractCall.__call__` + + This makes constant contract calls look more like transactions since we require + users to specify a dictionary as the last argument with the from field + being the multicall2 instance being used.""" + if args and isinstance(args[-1], dict): + args, tx = args[:-1], args[-1] + self = tx["from"] + return self._call_contract(*args, **kwargs) + + # standard call we let pass through + ContractCall.__call__.__code__ = getattr(ContractCall, "__original_call_code") + result = ContractCall.__call__(*args, **kwargs) # type: ignore + ContractCall.__call__.__code__ = getattr(ContractCall, "__proxy_call_code") + return result + + def __enter__(self) -> "Multicall2": + """Enter the Context Manager and substitute `ContractCall.__call__`""" + # we set the code objects on ContractCall class so we can grab them later + if not hasattr(ContractCall, "__original_call_code"): + setattr(ContractCall, "__original_call_code", ContractCall.__call__.__code__) + setattr(ContractCall, "__proxy_call_code", self._proxy_call.__code__) + ContractCall.__call__.__code__ = self._proxy_call.__code__ + self.flush() + self._complete = False + return self + + def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> None: + """Exit the Context Manager and reattach original `ContractCall.__call__` code""" + self.flush() + self._complete = True + ContractCall.__call__.__code__ = getattr(ContractCall, "__original_call_code") From 6d79ab4167cb62a4b3c9ac9f68f70a117a6a4a3f Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Fri, 25 Jun 2021 23:51:33 -0400 Subject: [PATCH 10/34] feat: add multicall2 to brownie namespace --- brownie/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/brownie/__init__.py b/brownie/__init__.py index c71e40452..497221691 100644 --- a/brownie/__init__.py +++ b/brownie/__init__.py @@ -8,11 +8,13 @@ from brownie.convert import Fixed, Wei from brownie.network import accounts, alert, chain, history, rpc, web3 from brownie.network.contract import Contract # NOQA: F401 +from brownie.network.multicall2 import Multicall2 ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" config = _CONFIG.settings +multicall2 = Multicall2 __all__ = [ "Contract", @@ -22,6 +24,7 @@ "alert", "chain", "history", # history is a TxHistory singleton + "multicall2", "network", "rpc", # rpc is a Rpc singleton "web3", # web3 is a Web3 instance From d72d9a157071fa180195932fcc99936869e629dc Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sat, 26 Jun 2021 09:47:01 -0400 Subject: [PATCH 11/34] fix: rename ctx mgr from multicall2 to multicall Now importable and used as brownie.multicall() --- brownie/__init__.py | 6 +-- .../network/{multicall2.py => multicall.py} | 4 +- .../{test_mulitcall2.py => test_mulitcall.py} | 38 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) rename brownie/network/{multicall2.py => multicall.py} (98%) rename tests/network/{test_mulitcall2.py => test_mulitcall.py} (75%) diff --git a/brownie/__init__.py b/brownie/__init__.py index 497221691..b5521a95d 100644 --- a/brownie/__init__.py +++ b/brownie/__init__.py @@ -8,13 +8,13 @@ from brownie.convert import Fixed, Wei from brownie.network import accounts, alert, chain, history, rpc, web3 from brownie.network.contract import Contract # NOQA: F401 -from brownie.network.multicall2 import Multicall2 +from brownie.network.multicall import Multicall ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" config = _CONFIG.settings -multicall2 = Multicall2 +multicall = Multicall __all__ = [ "Contract", @@ -24,7 +24,7 @@ "alert", "chain", "history", # history is a TxHistory singleton - "multicall2", + "multicall", "network", "rpc", # rpc is a Rpc singleton "web3", # web3 is a Web3 instance diff --git a/brownie/network/multicall2.py b/brownie/network/multicall.py similarity index 98% rename from brownie/network/multicall2.py rename to brownie/network/multicall.py index 3cec687d0..b0f8daeb6 100644 --- a/brownie/network/multicall2.py +++ b/brownie/network/multicall.py @@ -31,7 +31,7 @@ def __repr__(self) -> str: return repr(self.__wrapped__) -class Multicall2: +class Multicall: def __init__( self, address: str = None, block_identifier: Union[int, str, bytes] = None ) -> None: @@ -106,7 +106,7 @@ def _proxy_call(*args: Tuple, **kwargs: Dict[str, Any]) -> Any: ContractCall.__call__.__code__ = getattr(ContractCall, "__proxy_call_code") return result - def __enter__(self) -> "Multicall2": + def __enter__(self) -> "Multicall": """Enter the Context Manager and substitute `ContractCall.__call__`""" # we set the code objects on ContractCall class so we can grab them later if not hasattr(ContractCall, "__original_call_code"): diff --git a/tests/network/test_mulitcall2.py b/tests/network/test_mulitcall.py similarity index 75% rename from tests/network/test_mulitcall2.py rename to tests/network/test_mulitcall.py index 85c304839..391b1489b 100644 --- a/tests/network/test_mulitcall2.py +++ b/tests/network/test_mulitcall.py @@ -6,12 +6,12 @@ def test_auto_deploy_on_testnet(config, devnetwork): - with brownie.multicall2(): + with brownie.multicall(): # gets deployed on init assert "multicall2" in config.active_network addr = config.active_network["multicall2"] - with brownie.multicall2(): + with brownie.multicall(): # uses the previously deployed instance assert config.active_network["multicall2"] == addr @@ -21,11 +21,11 @@ def test_proxy_object_is_returned_from_calls(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall2() as mc2: + with brownie.multicall() as m: # the value hasn't been fetched so ret_value is just the proxy # but if we access ret_val again it will update # so use getattr_static to see it has yet to update - ret_val = tester.getTuple(addr, {"from": mc2}) + ret_val = tester.getTuple(addr, {"from": m}) assert inspect.getattr_static(ret_val, "__wrapped__") != value assert isinstance(ret_val, Proxy) assert ret_val.__wrapped__ == value @@ -36,11 +36,11 @@ def test_flush_mid_execution(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall2() as mc2: - tester.getTuple(addr, {"from": mc2}) - assert len(mc2._pending_calls) == 1 - mc2.flush() - assert len(mc2._pending_calls) == 0 + with brownie.multicall() as m: + tester.getTuple(addr, {"from": m}) + assert len(m._pending_calls) == 1 + m.flush() + assert len(m._pending_calls) == 0 def test_proxy_object_fetches_on_next_use(accounts, tester): @@ -48,12 +48,12 @@ def test_proxy_object_fetches_on_next_use(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall2() as mc2: - ret_val = tester.getTuple(addr, {"from": mc2}) - assert len(mc2._pending_calls) == 1 + with brownie.multicall() as m: + ret_val = tester.getTuple(addr, {"from": m}) + assert len(m._pending_calls) == 1 # ret_val is now fetched assert ret_val == value - assert len(mc2._pending_calls) == 0 + assert len(m._pending_calls) == 0 def test_proxy_object_updates_on_exit(accounts, tester): @@ -61,8 +61,8 @@ def test_proxy_object_updates_on_exit(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall2() as mc2: - ret_val = tester.getTuple(addr, {"from": mc2}) + with brownie.multicall() as m: + ret_val = tester.getTuple(addr, {"from": m}) assert ret_val == value @@ -72,7 +72,7 @@ def test_standard_calls_passthrough(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall2(): + with brownie.multicall(): assert tester.getTuple(addr) == value @@ -81,7 +81,7 @@ def test_standard_calls_work_after_context(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall2(): + with brownie.multicall(): assert tester.getTuple(addr) == value assert tester.getTuple(addr) == value @@ -92,9 +92,9 @@ def test_double_multicall(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall2() as mc1: + with brownie.multicall() as mc1: tester.getTuple(addr, {"from": mc1}) - with brownie.multicall2() as mc2: + with brownie.multicall() as mc2: mc2._contract.getCurrentBlockTimestamp({"from": mc2}) assert len(mc1._pending_calls) == 1 assert len(mc2._pending_calls) == 1 From 742ce6c3a9e02c75012d0e52dc640e8c15cbf7b6 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 27 Jun 2021 00:35:21 -0400 Subject: [PATCH 12/34] test: additional test cases Testing the deploy classmethod. Testing an error is raised when trying to use multicall contract which didn't exist at specified block number. --- tests/network/test_mulitcall.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/network/test_mulitcall.py b/tests/network/test_mulitcall.py index 391b1489b..82319a8b6 100644 --- a/tests/network/test_mulitcall.py +++ b/tests/network/test_mulitcall.py @@ -1,8 +1,10 @@ import inspect +import pytest from lazy_object_proxy import Proxy import brownie +from brownie.exceptions import ContractNotFound def test_auto_deploy_on_testnet(config, devnetwork): @@ -100,3 +102,34 @@ def test_double_multicall(accounts, tester): assert len(mc2._pending_calls) == 1 assert len(mc1._pending_calls) == 1 assert len(mc2._pending_calls) == 0 + + +def test_raises_for_ancient_block_identifier(accounts, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tx = tester.setTuple(value) + + with pytest.raises(ContractNotFound): + # block identifier is before multicall existed + with brownie.multicall(block_identifier=tx.block_number): + pass + + +def test_deploy_classmethod(accounts, config): + multicall = brownie.multicall.deploy({"from": accounts[0]}) + assert config.active_network["multicall2"] == multicall.address + + +def test_using_block_identifier(accounts, tester): + # need to deploy before progressing chain + brownie.multicall.deploy({"from": accounts[0]}) + + addr = accounts[1] + old_value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tx = tester.setTuple(old_value) + new_value = ["fooo", addr, ["nonono", "0x4321"]] + tester.setTuple(new_value) + + with brownie.multicall(block_identifier=tx.block_number) as m: + assert tester.getTuple(addr, {"from": m}) == old_value + assert tester.getTuple(addr) == new_value From 4ba02d0b1f07af0c95a26388db8f569758f6dbd0 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 27 Jun 2021 00:40:44 -0400 Subject: [PATCH 13/34] feat: add deploy fn and handle specifying block_identifier --- brownie/network/multicall.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index b0f8daeb6..0fa08f981 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -6,7 +6,7 @@ from lazy_object_proxy import Proxy from wrapt import ObjectProxy -from brownie import accounts +from brownie import accounts, web3 from brownie._config import BROWNIE_FOLDER, CONFIG from brownie.exceptions import ContractNotFound from brownie.network.contract import Contract, ContractCall @@ -48,14 +48,18 @@ def __init__( if "multicall2" in active_network: self.address = active_network["multicall2"] elif "cmd" in active_network: - # development or forked network - project = compile_source(MULTICALL2_SOURCE) - deployment = project.Multicall2.deploy({"from": accounts[-1]}) # type: ignore - self.address = active_network["multicall2"] = deployment.address + self.address = self.deploy({"from": accounts[0]}).address else: # live network and no address raise ContractNotFound("Must provide Multicall2 address as argument") + if not web3.eth.get_code(self.address, block_identifier): + # TODO: Handle deploying multicall in a test network without breaking the expected chain + # For Geth client's we can use state override to have multicall at any arbitrary address + raise ContractNotFound( + f"Multicall2 at `{self.address}` not available at block `{block_identifier}`" + ) + contract = Contract.from_abi("Multicall2", self.address, MULTICALL2_ABI) # type: ignore self._contract = contract @@ -66,7 +70,9 @@ def _flush(self, future_result: Result = None) -> Any: return future_result ContractCall.__call__.__code__ = getattr(ContractCall, "__original_call_code") results = self._contract.tryAggregate( - False, [_call.calldata for _call in self._pending_calls] + False, + [_call.calldata for _call in self._pending_calls], + block_identifier=self.block_identifier, ) if not self._complete: ContractCall.__call__.__code__ = getattr(ContractCall, "__proxy_call_code") @@ -122,3 +128,10 @@ def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> self.flush() self._complete = True ContractCall.__call__.__code__ = getattr(ContractCall, "__original_call_code") + + @classmethod + def deploy(cls, tx_params: Dict) -> Contract: + project = compile_source(MULTICALL2_SOURCE) + deployment = project.Multicall2.deploy(tx_params) # type: ignore + CONFIG.active_network["multicall2"] = deployment.address + return deployment From b21807f00807c0f7bfe693a9d0d97819a6d3c29d Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 27 Jun 2021 11:30:54 -0400 Subject: [PATCH 14/34] test: all calls come from the same block --- tests/network/test_mulitcall.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/network/test_mulitcall.py b/tests/network/test_mulitcall.py index 82319a8b6..f1979b4de 100644 --- a/tests/network/test_mulitcall.py +++ b/tests/network/test_mulitcall.py @@ -133,3 +133,18 @@ def test_using_block_identifier(accounts, tester): with brownie.multicall(block_identifier=tx.block_number) as m: assert tester.getTuple(addr, {"from": m}) == old_value assert tester.getTuple(addr) == new_value + + +def test_all_values_come_from_the_same_block(chain, devnetwork): + with brownie.multicall() as m: + first_call = m._contract.getBlockNumber({"from": m}) + chain.mine(10) + second_call = m._contract.getBlockNumber({"from": m}) + assert first_call == second_call + # pending calls have been flushed + third_call = m._contract.getBlockNumber({"from": m}) + chain.mine(10) + fourth_call = m._contract.getBlockNumber({"from": m}) + assert first_call == second_call == third_call == fourth_call + + assert m._contract.getBlockNumber() == first_call + 20 From 06ae659a5757528f843a47039caefbea271e4f7e Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 27 Jun 2021 11:31:52 -0400 Subject: [PATCH 15/34] fix: handle all calls are queried from the same block Previously, if the block_identifier was not specified and the pending calls queue was flushed, any future pending calls would come from a different block. To handle this, we now on init hardcode the block identifier to call from if not specified. Also handled is if the block identifier is specified in a dev network, and the address is not supplied, the user needs to deploy multicall2 first. (we raise a ContractNotFound error, since the contract was not found at user specified block height) --- brownie/network/multicall.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 0fa08f981..87fb6df1d 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -6,7 +6,7 @@ from lazy_object_proxy import Proxy from wrapt import ObjectProxy -from brownie import accounts, web3 +from brownie import accounts, chain, web3 from brownie._config import BROWNIE_FOLDER, CONFIG from brownie.exceptions import ContractNotFound from brownie.network.contract import Contract, ContractCall @@ -38,7 +38,7 @@ def __init__( super().__init__() self.address = address - self.block_identifier = block_identifier + self.block_identifier = block_identifier or chain.height self._pending_calls: List[Call] = [] self._complete = False @@ -48,16 +48,23 @@ def __init__( if "multicall2" in active_network: self.address = active_network["multicall2"] elif "cmd" in active_network: - self.address = self.deploy({"from": accounts[0]}).address + if block_identifier is not None: + raise ContractNotFound( + f"Must deploy Multicall2 before block {self.block_identifier}. " + "Use `Multicall2.deploy` classmethod to deploy an instance of Multicall2." + ) + deployment = self.deploy({"from": accounts[0]}) + self.address = deployment.address + self.block_identifier = deployment.tx.block_number # type: ignore else: # live network and no address raise ContractNotFound("Must provide Multicall2 address as argument") - if not web3.eth.get_code(self.address, block_identifier): + if not web3.eth.get_code(self.address, self.block_identifier): # TODO: Handle deploying multicall in a test network without breaking the expected chain # For Geth client's we can use state override to have multicall at any arbitrary address raise ContractNotFound( - f"Multicall2 at `{self.address}` not available at block `{block_identifier}`" + f"Multicall2 at `{self.address}` not available at block `{self.block_identifier}`" ) contract = Contract.from_abi("Multicall2", self.address, MULTICALL2_ABI) # type: ignore From b087a741daea9df3bd8c11b8ee4f6db3e8d6e088 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 27 Jun 2021 12:03:06 -0400 Subject: [PATCH 16/34] fix: repr of lazy results --- brownie/network/multicall.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 87fb6df1d..6c7bae201 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -31,12 +31,17 @@ def __repr__(self) -> str: return repr(self.__wrapped__) +class LazyResult(Proxy): + """A proxy object to be updated with the result of a multicall.""" + + def __repr__(self) -> str: + return repr(self.__wrapped__) + + class Multicall: def __init__( self, address: str = None, block_identifier: Union[int, str, bytes] = None ) -> None: - super().__init__() - self.address = address self.block_identifier = block_identifier or chain.height self._pending_calls: List[Call] = [] @@ -99,7 +104,7 @@ def _call_contract(self, call: ContractCall, *args: Tuple, **kwargs: Dict[str, A result = Result(call_obj) self._pending_calls.append(result) - return Proxy(lambda: self._flush(result)) + return LazyResult(lambda: self._flush(result)) @staticmethod def _proxy_call(*args: Tuple, **kwargs: Dict[str, Any]) -> Any: From 25f267563cad59627c6def9f59ac286837f41e3d Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 27 Jun 2021 12:29:13 -0400 Subject: [PATCH 17/34] fix: remove aliasing Multicall class --- brownie/__init__.py | 3 +-- brownie/network/multicall.py | 2 +- tests/network/test_mulitcall.py | 30 +++++++++++++++--------------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/brownie/__init__.py b/brownie/__init__.py index b5521a95d..e2262093d 100644 --- a/brownie/__init__.py +++ b/brownie/__init__.py @@ -14,7 +14,6 @@ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" config = _CONFIG.settings -multicall = Multicall __all__ = [ "Contract", @@ -24,7 +23,7 @@ "alert", "chain", "history", # history is a TxHistory singleton - "multicall", + "Multicall", "network", "rpc", # rpc is a Rpc singleton "web3", # web3 is a Web3 instance diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 6c7bae201..8028e5b32 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -45,7 +45,7 @@ def __init__( self.address = address self.block_identifier = block_identifier or chain.height self._pending_calls: List[Call] = [] - self._complete = False + self._complete = True if address is None: active_network = CONFIG.active_network diff --git a/tests/network/test_mulitcall.py b/tests/network/test_mulitcall.py index f1979b4de..265b61bfe 100644 --- a/tests/network/test_mulitcall.py +++ b/tests/network/test_mulitcall.py @@ -8,12 +8,12 @@ def test_auto_deploy_on_testnet(config, devnetwork): - with brownie.multicall(): + with brownie.Multicall(): # gets deployed on init assert "multicall2" in config.active_network addr = config.active_network["multicall2"] - with brownie.multicall(): + with brownie.Multicall(): # uses the previously deployed instance assert config.active_network["multicall2"] == addr @@ -23,7 +23,7 @@ def test_proxy_object_is_returned_from_calls(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall() as m: + with brownie.Multicall() as m: # the value hasn't been fetched so ret_value is just the proxy # but if we access ret_val again it will update # so use getattr_static to see it has yet to update @@ -38,7 +38,7 @@ def test_flush_mid_execution(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall() as m: + with brownie.Multicall() as m: tester.getTuple(addr, {"from": m}) assert len(m._pending_calls) == 1 m.flush() @@ -50,7 +50,7 @@ def test_proxy_object_fetches_on_next_use(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall() as m: + with brownie.Multicall() as m: ret_val = tester.getTuple(addr, {"from": m}) assert len(m._pending_calls) == 1 # ret_val is now fetched @@ -63,7 +63,7 @@ def test_proxy_object_updates_on_exit(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall() as m: + with brownie.Multicall() as m: ret_val = tester.getTuple(addr, {"from": m}) assert ret_val == value @@ -74,7 +74,7 @@ def test_standard_calls_passthrough(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall(): + with brownie.Multicall(): assert tester.getTuple(addr) == value @@ -83,7 +83,7 @@ def test_standard_calls_work_after_context(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall(): + with brownie.Multicall(): assert tester.getTuple(addr) == value assert tester.getTuple(addr) == value @@ -94,9 +94,9 @@ def test_double_multicall(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.multicall() as mc1: + with brownie.Multicall() as mc1: tester.getTuple(addr, {"from": mc1}) - with brownie.multicall() as mc2: + with brownie.Multicall() as mc2: mc2._contract.getCurrentBlockTimestamp({"from": mc2}) assert len(mc1._pending_calls) == 1 assert len(mc2._pending_calls) == 1 @@ -111,18 +111,18 @@ def test_raises_for_ancient_block_identifier(accounts, tester): with pytest.raises(ContractNotFound): # block identifier is before multicall existed - with brownie.multicall(block_identifier=tx.block_number): + with brownie.Multicall(block_identifier=tx.block_number): pass def test_deploy_classmethod(accounts, config): - multicall = brownie.multicall.deploy({"from": accounts[0]}) + multicall = brownie.Multicall.deploy({"from": accounts[0]}) assert config.active_network["multicall2"] == multicall.address def test_using_block_identifier(accounts, tester): # need to deploy before progressing chain - brownie.multicall.deploy({"from": accounts[0]}) + brownie.Multicall.deploy({"from": accounts[0]}) addr = accounts[1] old_value = ["blahblah", addr, ["yesyesyes", "0x1234"]] @@ -130,13 +130,13 @@ def test_using_block_identifier(accounts, tester): new_value = ["fooo", addr, ["nonono", "0x4321"]] tester.setTuple(new_value) - with brownie.multicall(block_identifier=tx.block_number) as m: + with brownie.Multicall(block_identifier=tx.block_number) as m: assert tester.getTuple(addr, {"from": m}) == old_value assert tester.getTuple(addr) == new_value def test_all_values_come_from_the_same_block(chain, devnetwork): - with brownie.multicall() as m: + with brownie.Multicall() as m: first_call = m._contract.getBlockNumber({"from": m}) chain.mine(10) second_call = m._contract.getBlockNumber({"from": m}) From 51334bac8c4aa738cf09a15fea23858f086c1abd Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 27 Jun 2021 23:54:37 -0400 Subject: [PATCH 18/34] test: multicall block_number getter + setter --- tests/network/test_mulitcall.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/network/test_mulitcall.py b/tests/network/test_mulitcall.py index 265b61bfe..c60abca76 100644 --- a/tests/network/test_mulitcall.py +++ b/tests/network/test_mulitcall.py @@ -148,3 +148,32 @@ def test_all_values_come_from_the_same_block(chain, devnetwork): assert first_call == second_call == third_call == fourth_call assert m._contract.getBlockNumber() == first_call + 20 + + +def test_block_number_getter_setter(chain, devnetwork): + multicall = brownie.Multicall() + + assert multicall.block_number == chain.height + chain.mine(10) + multicall.block_number += 10 + assert multicall.block_number == chain.height + + +def test_reusing_multicall(accounts, chain, tester): + addr = accounts[1] + value = ["blahblah", addr, ["yesyesyes", "0x1234"]] + tester.setTuple(value) + + multicall = brownie.Multicall() + + with multicall as m: + assert tester.getTuple(addr, {"from": m}) == value + + chain.mine(9) + new_value = ["fooo", addr, ["nonono", "0x4321"]] + tester.setTuple(new_value) + + multicall.block_number += 10 + + with multicall as m: + assert tester.getTuple(addr, {"from": m}) == new_value From 90479aa433c8f6243dde2847065f98a3e93feafb Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 27 Jun 2021 23:55:03 -0400 Subject: [PATCH 19/34] feat: block number getter + setter --- brownie/network/multicall.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 8028e5b32..b3b0cd22d 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -39,11 +39,13 @@ def __repr__(self) -> str: class Multicall: + """Context manager for batching multiple calls to constant contract functions.""" + def __init__( self, address: str = None, block_identifier: Union[int, str, bytes] = None ) -> None: self.address = address - self.block_identifier = block_identifier or chain.height + self._block_identifier = block_identifier or chain.height self._pending_calls: List[Call] = [] self._complete = True @@ -55,21 +57,21 @@ def __init__( elif "cmd" in active_network: if block_identifier is not None: raise ContractNotFound( - f"Must deploy Multicall2 before block {self.block_identifier}. " + f"Must deploy Multicall2 before block {self._block_identifier}. " "Use `Multicall2.deploy` classmethod to deploy an instance of Multicall2." ) deployment = self.deploy({"from": accounts[0]}) self.address = deployment.address - self.block_identifier = deployment.tx.block_number # type: ignore + self._block_identifier = deployment.tx.block_number # type: ignore else: # live network and no address raise ContractNotFound("Must provide Multicall2 address as argument") - if not web3.eth.get_code(self.address, self.block_identifier): + if not web3.eth.get_code(self.address, self._block_identifier): # TODO: Handle deploying multicall in a test network without breaking the expected chain # For Geth client's we can use state override to have multicall at any arbitrary address raise ContractNotFound( - f"Multicall2 at `{self.address}` not available at block `{self.block_identifier}`" + f"Multicall2 at `{self.address}` not available at block `{self._block_identifier}`" ) contract = Contract.from_abi("Multicall2", self.address, MULTICALL2_ABI) # type: ignore @@ -84,7 +86,7 @@ def _flush(self, future_result: Result = None) -> Any: results = self._contract.tryAggregate( False, [_call.calldata for _call in self._pending_calls], - block_identifier=self.block_identifier, + block_identifier=self._block_identifier, ) if not self._complete: ContractCall.__call__.__code__ = getattr(ContractCall, "__proxy_call_code") @@ -141,6 +143,19 @@ def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> self._complete = True ContractCall.__call__.__code__ = getattr(ContractCall, "__original_call_code") + @property + def block_number(self) -> Union[int, str, bytes]: + return self._block_identifier + + @block_number.setter + def block_number(self, value: Union[int, str, bytes]) -> None: + self.flush() + if not web3.eth.get_code(self.address, self._block_identifier): + raise ContractNotFound( + f"Multicall2 at `{self.address}` not available at block `{value}`" + ) + self._block_identifier = value + @classmethod def deploy(cls, tx_params: Dict) -> Contract: project = compile_source(MULTICALL2_SOURCE) From 18e44805d4ec478a42547a15c99ec1ef36f837e4 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 30 Jun 2021 02:36:16 -0400 Subject: [PATCH 20/34] docs: add documentation for multicall --- docs/api-network.rst | 105 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/docs/api-network.rst b/docs/api-network.rst index 647108a88..f0ca3b1cb 100644 --- a/docs/api-network.rst +++ b/docs/api-network.rst @@ -1662,6 +1662,111 @@ To implement a scaling strategy, subclass one of the above ABCs and implement th The produced generator is called every ``duration`` seconds while a transaction is still pending. Each call must yield a new gas price as an integer. If the newly yielded value is at least 10% higher than the current gas price, the transaction is rebroadcasted with the new gas price. +``brownie.network.multicall`` +============================= + +The ``multicall`` module contains the :func:`Multicall ` context manager, which allows for the batching of multiple constant contract function calls via ``Multicall2``. + +Multicall +--------- + +.. py:class:: brownie.network.multicall.Multicall(address=None, block_identifier=None) + + Instances of ``Multicall`` allow for the batching of constant contract function calls through a modified version of the standart Brownie call API. + + The only syntatic difference between a multicall and a standard brownie contract function call is the final argument for a multicall, is a dictionary with the ``from`` key being the instance of ``Multicall`` being used. + + Features: + + 1. Uses a modified but familiar call API + 2. Lazy fetching of results + 3. Auto-deployment on development networks (on first use). + 4. Uses ``multicall2`` key in network-config as pre-defined multicall contract address + 5. Can specify/modify block number to make calls at particular block heights + 6. Calls which fail return ``None`` instad of causing all calls to fail + + .. code-block:: python + + >>> import brownie + >>> from brownie import Contract + >>> addr_provider = Contract("0x0000000022D53366457F9d5E68Ec105046FC4383") + >>> registry = Contract(addr_provider.get_registry()) + >>> with brownie.Multicall() as m: + ... pool_count = registry.pool_count() # standard call, no batching + ... pools = [registry.pool_list(i, {"from": m}) for i in range(pool_count)] # batched + ... gauges = [registry.get_gauges(pool, {"from": m}) for pool in pools] # batched + ... print(*zip(pools, gauges), sep="\n") + +Multicall Attributes +******************** + +.. py:attribute:: Multicall.address + + The deployed ``Multicall2`` contract address used for batching calls. + + .. code-block:: python + + >>> Multicall().address + 0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696 + >>> Multicall("0xc8E51042792d7405184DfCa245F2d27B94D013b6").address + 0xc8E51042792d7405184DfCa245F2d27B94D013b6 + +.. py:attribute:: Multicall.block_number + + The block height which call results are aggregated from. + + .. note:: + + ``Multicall`` relies on an instance of ``Multicall2`` being available for aggregating results. If you set the block_height before the ``Multicall2`` instance you are using was deployed a ``ContractNotFound`` error will be raised. + + .. note:: + + You can modify the block height used within the context manager, this will flush the queue of currently pending calls, and then adjust to the new block height + + .. code-block:: python + + >>> m = Multicall() + >>> m.block_number + 12733683 + +Multicall Methods +***************** + +.. py:classmethod:: Multicall.deploy + + Deploys an instance of ``Multicall2``, especially useful when creating fixutes for testing. + + .. code-block:: python + + >>> multicall2 = Multicall.deploy({"from": alice}) + + +.. py:classmethod:: Multicall.flush + + Flushes the current queue of pending calls, especially useful for preventing ``OOG`` errors from occuring when querying large amounts of data. + + >>> results = [] + >>> long_list_of_addresses = [...] + >>> token = Contract(...) + >>> with Multicall() as m: + ... for i, addr in enumerate(long_list_of_addresses): + ... if i % 1_000: + ... m.flush() + ... results.append(token.balanceOf(addr)) + +Multicall Internal Attributes +***************************** + +.. py:attribute:: Multicall._contract + + The contract instance of ``Multicall2`` used to query data + +.. py:attribute:: Multicall._pending_calls + + List of proxy objects representing calls to be made. While pending, these calls contain the data necessary to make an aggregate call with multicall and also decode the result. + + + ``brownie.network.state`` ========================= From 33b0d84c0637d79c7278eaac24a828603da02f8b Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 30 Jun 2021 02:57:44 -0400 Subject: [PATCH 21/34] test: modify test to simulate multi-instance safety --- tests/network/test_mulitcall.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/network/test_mulitcall.py b/tests/network/test_mulitcall.py index c60abca76..e4d6ec663 100644 --- a/tests/network/test_mulitcall.py +++ b/tests/network/test_mulitcall.py @@ -100,7 +100,8 @@ def test_double_multicall(accounts, tester): mc2._contract.getCurrentBlockTimestamp({"from": mc2}) assert len(mc1._pending_calls) == 1 assert len(mc2._pending_calls) == 1 - assert len(mc1._pending_calls) == 1 + tester.getTuple(addr, {"from": mc1}) + assert len(mc1._pending_calls) == 2 assert len(mc2._pending_calls) == 0 From 781536cd3427cfda78b8f437bc216d46ff24555a Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 30 Jun 2021 03:18:41 -0400 Subject: [PATCH 22/34] fix: remove replacing original code object back I guess this means once instantiated and used, __call__ doesn't work as expected anymore and is fully monkeypatched ... I suspect adding in a hook into the ContractCall class would be a better alternative than this monkeypatching, and less prone to error in the future. Should look into refactoring completely. --- brownie/network/multicall.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index b3b0cd22d..2436ce00c 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -47,7 +47,6 @@ def __init__( self.address = address self._block_identifier = block_identifier or chain.height self._pending_calls: List[Call] = [] - self._complete = True if address is None: active_network = CONFIG.active_network @@ -88,8 +87,7 @@ def _flush(self, future_result: Result = None) -> Any: [_call.calldata for _call in self._pending_calls], block_identifier=self._block_identifier, ) - if not self._complete: - ContractCall.__call__.__code__ = getattr(ContractCall, "__proxy_call_code") + ContractCall.__call__.__code__ = getattr(ContractCall, "__proxy_call_code") for _call, result in zip(self._pending_calls, results): _call.__wrapped__ = _call.decoder(result[1]) if result[0] else None # type: ignore self._pending_calls = [] # empty the pending calls @@ -134,14 +132,11 @@ def __enter__(self) -> "Multicall": setattr(ContractCall, "__proxy_call_code", self._proxy_call.__code__) ContractCall.__call__.__code__ = self._proxy_call.__code__ self.flush() - self._complete = False return self def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> None: """Exit the Context Manager and reattach original `ContractCall.__call__` code""" self.flush() - self._complete = True - ContractCall.__call__.__code__ = getattr(ContractCall, "__original_call_code") @property def block_number(self) -> Union[int, str, bytes]: From afa394382a044a382882efe35009fd6cd74d2d2d Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Wed, 30 Jun 2021 03:21:44 -0400 Subject: [PATCH 23/34] chore: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d353b130c..84a20515f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add polygon network integration ([#1119](https://github.com/eth-brownie/brownie/pull/1119)) - Fixed subcalls to empty accounts not appearing in the subcalls property of TransactionReceipts ([#1106](https://github.com/eth-brownie/brownie/pull/1106)) - Add support for `POLYGONSCAN_TOKEN` env var ([#1135](https://github.com/eth-brownie/brownie/pull/1135)) +- Add Multicall context manager ([#1125](https://github.com/eth-brownie/brownie/pull/1125)) ### Added - Added `LocalAccount.sign_message` method to sign `EIP712Message` objects ([#1097](https://github.com/eth-brownie/brownie/pull/1097)) From 0192311253fc42c52410dd930b3d83d6734b0c50 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Thu, 1 Jul 2021 11:06:20 -0400 Subject: [PATCH 24/34] docs: add docstrings to Multicall class --- brownie/network/multicall.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 2436ce00c..67dfc2054 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -39,7 +39,21 @@ def __repr__(self) -> str: class Multicall: - """Context manager for batching multiple calls to constant contract functions.""" + """Context manager for batching multiple calls to constant contract functions. + + Args: + address: The address of an instance of the `Mulitcall2` contract. If `None`, uses + the default address in the network config, under the optional key `multicall2`. + block_identifier: The block number which to aggregate calls from, defaults to the + current block number at the moment of instantiation. + + Attributes: + block_number: The block number calls will be aggregated from + + Raises: + ContractNotFound: If `Multicall2` was not deployed at `address` at the block number + specified by `block_identifier`. + """ def __init__( self, address: str = None, block_identifier: Union[int, str, bytes] = None @@ -94,6 +108,7 @@ def _flush(self, future_result: Result = None) -> Any: return future_result def flush(self) -> Any: + """Flush the pending queue of calls, retrieving all the results.""" return self._flush() def _call_contract(self, call: ContractCall, *args: Tuple, **kwargs: Dict[str, Any]) -> Proxy: @@ -140,6 +155,7 @@ def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> @property def block_number(self) -> Union[int, str, bytes]: + """The block number calls are aggregated from.""" return self._block_identifier @block_number.setter @@ -153,6 +169,12 @@ def block_number(self, value: Union[int, str, bytes]) -> None: @classmethod def deploy(cls, tx_params: Dict) -> Contract: + """Deploy an instance of the `Multicall2` contract. + + Args: + tx_params: parameters passed to the `deploy` method of the `Multicall2` contract + container. + """ project = compile_source(MULTICALL2_SOURCE) deployment = project.Multicall2.deploy(tx_params) # type: ignore CONFIG.active_network["multicall2"] = deployment.address From 3096c30277cb0059c0ea3fa08aed22e70c57965f Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 13:28:25 -0400 Subject: [PATCH 25/34] fix: update init fn --- brownie/network/multicall.py | 59 ++++++++---------------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 67dfc2054..7f335b80f 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -39,56 +39,21 @@ def __repr__(self) -> str: class Multicall: - """Context manager for batching multiple calls to constant contract functions. - - Args: - address: The address of an instance of the `Mulitcall2` contract. If `None`, uses - the default address in the network config, under the optional key `multicall2`. - block_identifier: The block number which to aggregate calls from, defaults to the - current block number at the moment of instantiation. - - Attributes: - block_number: The block number calls will be aggregated from - - Raises: - ContractNotFound: If `Multicall2` was not deployed at `address` at the block number - specified by `block_identifier`. - """ - - def __init__( - self, address: str = None, block_identifier: Union[int, str, bytes] = None - ) -> None: - self.address = address - self._block_identifier = block_identifier or chain.height - self._pending_calls: List[Call] = [] + """Context manager for batching multiple calls to constant contract functions.""" - if address is None: - active_network = CONFIG.active_network - - if "multicall2" in active_network: - self.address = active_network["multicall2"] - elif "cmd" in active_network: - if block_identifier is not None: - raise ContractNotFound( - f"Must deploy Multicall2 before block {self._block_identifier}. " - "Use `Multicall2.deploy` classmethod to deploy an instance of Multicall2." - ) - deployment = self.deploy({"from": accounts[0]}) - self.address = deployment.address - self._block_identifier = deployment.tx.block_number # type: ignore - else: - # live network and no address - raise ContractNotFound("Must provide Multicall2 address as argument") + def __init__(self) -> None: + self._address = None + self._block_identifier = None + self._contract = None + self._pending_calls: List[Call] = [] - if not web3.eth.get_code(self.address, self._block_identifier): - # TODO: Handle deploying multicall in a test network without breaking the expected chain - # For Geth client's we can use state override to have multicall at any arbitrary address - raise ContractNotFound( - f"Multicall2 at `{self.address}` not available at block `{self._block_identifier}`" - ) + active_network = CONFIG.active_network - contract = Contract.from_abi("Multicall2", self.address, MULTICALL2_ABI) # type: ignore - self._contract = contract + if "multicall2" in active_network: + self._address = active_network["multicall2"] + elif "cmd" in active_network: + deployment = self.deploy({"from": accounts[0]}) + self._address = deployment.address def _flush(self, future_result: Result = None) -> Any: if not self._pending_calls: From 7cda17858e605abc65f64f9718a9b7bff23381c7 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 13:40:07 -0400 Subject: [PATCH 26/34] fix: update usage --- brownie/network/multicall.py | 55 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 7f335b80f..920912dc6 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -10,6 +10,8 @@ from brownie._config import BROWNIE_FOLDER, CONFIG from brownie.exceptions import ContractNotFound from brownie.network.contract import Contract, ContractCall +from threading import get_ident +from collections import defaultdict from brownie.project import compile_source DATA_DIR = BROWNIE_FOLDER.joinpath("data") @@ -55,6 +57,16 @@ def __init__(self) -> None: deployment = self.deploy({"from": accounts[0]}) self._address = deployment.address + ContractCall.__original_call_code = ContractCall.__call__.__code__ + ContractCall.__proxy_call_code = self._proxy_call.__code__ + ContractCall.__call__.__code__ = self._proxy_call.__code__ + ContractCall.__multicall = defaultdict(lambda: None) + + def __call__(self, address: str, block_identifer: Union[str, bytes, int] = None) -> "Multicall": + self._address = address + self._block_identifier = block_identifer + return self + def _flush(self, future_result: Result = None) -> Any: if not self._pending_calls: # either all calls have already been made @@ -93,9 +105,10 @@ def _proxy_call(*args: Tuple, **kwargs: Dict[str, Any]) -> Any: This makes constant contract calls look more like transactions since we require users to specify a dictionary as the last argument with the from field being the multicall2 instance being used.""" - if args and isinstance(args[-1], dict): - args, tx = args[:-1], args[-1] - self = tx["from"] + from threading import get_ident + + self = ContractCall.__multicall[get_ident()] + if self: return self._call_contract(*args, **kwargs) # standard call we let pass through @@ -107,33 +120,25 @@ def _proxy_call(*args: Tuple, **kwargs: Dict[str, Any]) -> Any: def __enter__(self) -> "Multicall": """Enter the Context Manager and substitute `ContractCall.__call__`""" # we set the code objects on ContractCall class so we can grab them later - if not hasattr(ContractCall, "__original_call_code"): - setattr(ContractCall, "__original_call_code", ContractCall.__call__.__code__) - setattr(ContractCall, "__proxy_call_code", self._proxy_call.__code__) - ContractCall.__call__.__code__ = self._proxy_call.__code__ - self.flush() - return self + self._block_identifier = self._block_identifier or web3.eth.get_block_number() - def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> None: - """Exit the Context Manager and reattach original `ContractCall.__call__` code""" - self.flush() + if self._address == None: + raise ContractNotFound( + "Must set Multicall address via `brownie.multicall(address=...)`" + ) + elif not web3.eth.get_code(self._address, block_identifier=self._block_identifier): + raise ContractNotFound( + f"Multicall at address {self._address} does not exit at block {self._block_identifier}" + ) - @property - def block_number(self) -> Union[int, str, bytes]: - """The block number calls are aggregated from.""" - return self._block_identifier + self._contract = Contract.from_abi("Multicall", self._address, MULTICALL2_ABI) - @block_number.setter - def block_number(self, value: Union[int, str, bytes]) -> None: + def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> None: + """Exit the Context Manager and reattach original `ContractCall.__call__` code""" self.flush() - if not web3.eth.get_code(self.address, self._block_identifier): - raise ContractNotFound( - f"Multicall2 at `{self.address}` not available at block `{value}`" - ) - self._block_identifier = value - @classmethod - def deploy(cls, tx_params: Dict) -> Contract: + @staticmethod + def deploy(tx_params: Dict) -> Contract: """Deploy an instance of the `Multicall2` contract. Args: From 222b19f8d4997eceae937efcf3f3fca8e94d37a2 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 13:40:55 -0400 Subject: [PATCH 27/34] fix: namespacing --- brownie/__init__.py | 5 +++-- brownie/network/multicall.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/brownie/__init__.py b/brownie/__init__.py index e2262093d..6fa5127aa 100644 --- a/brownie/__init__.py +++ b/brownie/__init__.py @@ -8,12 +8,13 @@ from brownie.convert import Fixed, Wei from brownie.network import accounts, alert, chain, history, rpc, web3 from brownie.network.contract import Contract # NOQA: F401 -from brownie.network.multicall import Multicall +from brownie.network.multicall import Multicall as _Multicall ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" config = _CONFIG.settings +multicall = _Multicall() __all__ = [ "Contract", @@ -23,7 +24,7 @@ "alert", "chain", "history", # history is a TxHistory singleton - "Multicall", + "multicall", "network", "rpc", # rpc is a Rpc singleton "web3", # web3 is a Web3 instance diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 920912dc6..ba0c191b3 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -132,10 +132,12 @@ def __enter__(self) -> "Multicall": ) self._contract = Contract.from_abi("Multicall", self._address, MULTICALL2_ABI) + ContractCall.__multicall[get_ident()] = self def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> None: """Exit the Context Manager and reattach original `ContractCall.__call__` code""" self.flush() + ContractCall.__multicall[get_ident()] = None @staticmethod def deploy(tx_params: Dict) -> Contract: From b1bc173bbdb02869287ce0834398cb90eb60148c Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 14:14:45 -0400 Subject: [PATCH 28/34] fix: attribute setting and calls --- brownie/__init__.py | 4 ++-- brownie/network/multicall.py | 41 ++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/brownie/__init__.py b/brownie/__init__.py index 6fa5127aa..79a007650 100644 --- a/brownie/__init__.py +++ b/brownie/__init__.py @@ -8,13 +8,13 @@ from brownie.convert import Fixed, Wei from brownie.network import accounts, alert, chain, history, rpc, web3 from brownie.network.contract import Contract # NOQA: F401 -from brownie.network.multicall import Multicall as _Multicall +from brownie.network import multicall as _multicall ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" config = _CONFIG.settings -multicall = _Multicall() +multicall = _multicall.Multicall() __all__ = [ "Contract", diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index ba0c191b3..48ee76348 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -4,9 +4,10 @@ from typing import Any, Dict, List, Tuple, Union from lazy_object_proxy import Proxy +from toolz.itertoolz import get from wrapt import ObjectProxy -from brownie import accounts, chain, web3 +from brownie.network import accounts, web3 from brownie._config import BROWNIE_FOLDER, CONFIG from brownie.exceptions import ContractNotFound from brownie.network.contract import Contract, ContractCall @@ -49,22 +50,16 @@ def __init__(self) -> None: self._contract = None self._pending_calls: List[Call] = [] - active_network = CONFIG.active_network - - if "multicall2" in active_network: - self._address = active_network["multicall2"] - elif "cmd" in active_network: - deployment = self.deploy({"from": accounts[0]}) - self._address = deployment.address - - ContractCall.__original_call_code = ContractCall.__call__.__code__ - ContractCall.__proxy_call_code = self._proxy_call.__code__ + setattr(ContractCall, "__original_call_code", ContractCall.__call__.__code__) + setattr(ContractCall, "__proxy_call_code", self._proxy_call.__code__) + setattr(ContractCall, "__multicall", defaultdict(lambda: None)) ContractCall.__call__.__code__ = self._proxy_call.__code__ - ContractCall.__multicall = defaultdict(lambda: None) - def __call__(self, address: str, block_identifer: Union[str, bytes, int] = None) -> "Multicall": + def __call__( + self, address: str = None, block_identifier: Union[str, bytes, int] = None + ) -> "Multicall": self._address = address - self._block_identifier = block_identifer + self._block_identifier = block_identifier return self def _flush(self, future_result: Result = None) -> Any: @@ -107,7 +102,7 @@ def _proxy_call(*args: Tuple, **kwargs: Dict[str, Any]) -> Any: being the multicall2 instance being used.""" from threading import get_ident - self = ContractCall.__multicall[get_ident()] + self = getattr(ContractCall, "__multicall", {}).get(get_ident()) if self: return self._call_contract(*args, **kwargs) @@ -120,6 +115,16 @@ def _proxy_call(*args: Tuple, **kwargs: Dict[str, Any]) -> Any: def __enter__(self) -> "Multicall": """Enter the Context Manager and substitute `ContractCall.__call__`""" # we set the code objects on ContractCall class so we can grab them later + + active_network = CONFIG.active_network + + if "multicall2" in active_network: + self._address = active_network["multicall2"] + elif "cmd" in active_network: + deployment = self.deploy({"from": accounts[0]}) + self._address = deployment.address + self._block_identifier = deployment.tx.block_number + self._block_identifier = self._block_identifier or web3.eth.get_block_number() if self._address == None: @@ -128,16 +133,16 @@ def __enter__(self) -> "Multicall": ) elif not web3.eth.get_code(self._address, block_identifier=self._block_identifier): raise ContractNotFound( - f"Multicall at address {self._address} does not exit at block {self._block_identifier}" + f"Multicall at address {self._address} does not exist at block {self._block_identifier}" ) self._contract = Contract.from_abi("Multicall", self._address, MULTICALL2_ABI) - ContractCall.__multicall[get_ident()] = self + getattr(ContractCall, "__multicall")[get_ident()] = self def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> None: """Exit the Context Manager and reattach original `ContractCall.__call__` code""" self.flush() - ContractCall.__multicall[get_ident()] = None + getattr(ContractCall, "__multicall")[get_ident()] = None @staticmethod def deploy(tx_params: Dict) -> Contract: From a90415efcd6ee8027e8c9b15d9bbda58b4576708 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 14:14:59 -0400 Subject: [PATCH 29/34] fix(test): update tests for correct usage --- tests/network/test_mulitcall.py | 116 +++++++++----------------------- 1 file changed, 31 insertions(+), 85 deletions(-) diff --git a/tests/network/test_mulitcall.py b/tests/network/test_mulitcall.py index e4d6ec663..2cf0448ec 100644 --- a/tests/network/test_mulitcall.py +++ b/tests/network/test_mulitcall.py @@ -8,12 +8,12 @@ def test_auto_deploy_on_testnet(config, devnetwork): - with brownie.Multicall(): + with brownie.multicall: # gets deployed on init assert "multicall2" in config.active_network addr = config.active_network["multicall2"] - with brownie.Multicall(): + with brownie.multicall: # uses the previously deployed instance assert config.active_network["multicall2"] == addr @@ -23,11 +23,11 @@ def test_proxy_object_is_returned_from_calls(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.Multicall() as m: + with brownie.multicall: # the value hasn't been fetched so ret_value is just the proxy # but if we access ret_val again it will update # so use getattr_static to see it has yet to update - ret_val = tester.getTuple(addr, {"from": m}) + ret_val = tester.getTuple(addr) assert inspect.getattr_static(ret_val, "__wrapped__") != value assert isinstance(ret_val, Proxy) assert ret_val.__wrapped__ == value @@ -38,11 +38,11 @@ def test_flush_mid_execution(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.Multicall() as m: - tester.getTuple(addr, {"from": m}) - assert len(m._pending_calls) == 1 - m.flush() - assert len(m._pending_calls) == 0 + with brownie.multicall: + tester.getTuple(addr) + assert len(brownie.multicall._pending_calls) == 1 + brownie.multicall.flush() + assert len(brownie.multicall._pending_calls) == 0 def test_proxy_object_fetches_on_next_use(accounts, tester): @@ -50,12 +50,12 @@ def test_proxy_object_fetches_on_next_use(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.Multicall() as m: - ret_val = tester.getTuple(addr, {"from": m}) - assert len(m._pending_calls) == 1 + with brownie.multicall: + ret_val = tester.getTuple(addr) + assert len(brownie.multicall._pending_calls) == 1 # ret_val is now fetched assert ret_val == value - assert len(m._pending_calls) == 0 + assert len(brownie.multicall._pending_calls) == 0 def test_proxy_object_updates_on_exit(accounts, tester): @@ -63,8 +63,8 @@ def test_proxy_object_updates_on_exit(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.Multicall() as m: - ret_val = tester.getTuple(addr, {"from": m}) + with brownie.multicall: + ret_val = tester.getTuple(addr) assert ret_val == value @@ -74,8 +74,9 @@ def test_standard_calls_passthrough(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.Multicall(): - assert tester.getTuple(addr) == value + with brownie.multicall: + assert tester.getTuple.call(addr) == value + assert not isinstance(tester.getTuple.call(addr), Proxy) def test_standard_calls_work_after_context(accounts, tester): @@ -83,47 +84,21 @@ def test_standard_calls_work_after_context(accounts, tester): value = ["blahblah", addr, ["yesyesyes", "0x1234"]] tester.setTuple(value) - with brownie.Multicall(): + with brownie.multicall: assert tester.getTuple(addr) == value assert tester.getTuple(addr) == value + assert not isinstance(tester.getTuple(addr), Proxy) -def test_double_multicall(accounts, tester): - addr = accounts[1] - value = ["blahblah", addr, ["yesyesyes", "0x1234"]] - tester.setTuple(value) - - with brownie.Multicall() as mc1: - tester.getTuple(addr, {"from": mc1}) - with brownie.Multicall() as mc2: - mc2._contract.getCurrentBlockTimestamp({"from": mc2}) - assert len(mc1._pending_calls) == 1 - assert len(mc2._pending_calls) == 1 - tester.getTuple(addr, {"from": mc1}) - assert len(mc1._pending_calls) == 2 - assert len(mc2._pending_calls) == 0 - - -def test_raises_for_ancient_block_identifier(accounts, tester): - addr = accounts[1] - value = ["blahblah", addr, ["yesyesyes", "0x1234"]] - tx = tester.setTuple(value) - - with pytest.raises(ContractNotFound): - # block identifier is before multicall existed - with brownie.Multicall(block_identifier=tx.block_number): - pass - - -def test_deploy_classmethod(accounts, config): - multicall = brownie.Multicall.deploy({"from": accounts[0]}) +def test_deploy_staticmethod(accounts, config): + multicall = brownie.multicall.deploy({"from": accounts[0]}) assert config.active_network["multicall2"] == multicall.address def test_using_block_identifier(accounts, tester): # need to deploy before progressing chain - brownie.Multicall.deploy({"from": accounts[0]}) + brownie.multicall.deploy({"from": accounts[0]}) addr = accounts[1] old_value = ["blahblah", addr, ["yesyesyes", "0x1234"]] @@ -131,50 +106,21 @@ def test_using_block_identifier(accounts, tester): new_value = ["fooo", addr, ["nonono", "0x4321"]] tester.setTuple(new_value) - with brownie.Multicall(block_identifier=tx.block_number) as m: - assert tester.getTuple(addr, {"from": m}) == old_value + with brownie.multicall(block_identifier=tx.block_number): + assert tester.getTuple(addr) == old_value assert tester.getTuple(addr) == new_value def test_all_values_come_from_the_same_block(chain, devnetwork): - with brownie.Multicall() as m: - first_call = m._contract.getBlockNumber({"from": m}) + with brownie.multicall: + first_call = brownie.multicall._contract.getBlockNumber() chain.mine(10) - second_call = m._contract.getBlockNumber({"from": m}) + second_call = brownie.multicall._contract.getBlockNumber() assert first_call == second_call # pending calls have been flushed - third_call = m._contract.getBlockNumber({"from": m}) + third_call = brownie.multicall._contract.getBlockNumber() chain.mine(10) - fourth_call = m._contract.getBlockNumber({"from": m}) + fourth_call = brownie.multicall._contract.getBlockNumber() assert first_call == second_call == third_call == fourth_call - assert m._contract.getBlockNumber() == first_call + 20 - - -def test_block_number_getter_setter(chain, devnetwork): - multicall = brownie.Multicall() - - assert multicall.block_number == chain.height - chain.mine(10) - multicall.block_number += 10 - assert multicall.block_number == chain.height - - -def test_reusing_multicall(accounts, chain, tester): - addr = accounts[1] - value = ["blahblah", addr, ["yesyesyes", "0x1234"]] - tester.setTuple(value) - - multicall = brownie.Multicall() - - with multicall as m: - assert tester.getTuple(addr, {"from": m}) == value - - chain.mine(9) - new_value = ["fooo", addr, ["nonono", "0x4321"]] - tester.setTuple(new_value) - - multicall.block_number += 10 - - with multicall as m: - assert tester.getTuple(addr, {"from": m}) == new_value + assert brownie.multicall._contract.getBlockNumber() == first_call + 20 \ No newline at end of file From 9da7e5d614b6178bc296815579839b52417341da Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 14:19:29 -0400 Subject: [PATCH 30/34] fix: public attributes --- brownie/network/multicall.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 48ee76348..ce9c2121f 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -45,8 +45,8 @@ class Multicall: """Context manager for batching multiple calls to constant contract functions.""" def __init__(self) -> None: - self._address = None - self._block_identifier = None + self.address = None + self.block_number = None self._contract = None self._pending_calls: List[Call] = [] @@ -58,8 +58,8 @@ def __init__(self) -> None: def __call__( self, address: str = None, block_identifier: Union[str, bytes, int] = None ) -> "Multicall": - self._address = address - self._block_identifier = block_identifier + self.address = address + self.block_number = block_identifier return self def _flush(self, future_result: Result = None) -> Any: @@ -71,7 +71,7 @@ def _flush(self, future_result: Result = None) -> Any: results = self._contract.tryAggregate( False, [_call.calldata for _call in self._pending_calls], - block_identifier=self._block_identifier, + block_identifier=self.block_number, ) ContractCall.__call__.__code__ = getattr(ContractCall, "__proxy_call_code") for _call, result in zip(self._pending_calls, results): @@ -119,24 +119,24 @@ def __enter__(self) -> "Multicall": active_network = CONFIG.active_network if "multicall2" in active_network: - self._address = active_network["multicall2"] + self.address = active_network["multicall2"] elif "cmd" in active_network: deployment = self.deploy({"from": accounts[0]}) - self._address = deployment.address - self._block_identifier = deployment.tx.block_number + self.address = deployment.address + self.block_number = deployment.tx.block_number - self._block_identifier = self._block_identifier or web3.eth.get_block_number() + self.block_number = self.block_number or web3.eth.get_block_number() - if self._address == None: + if self.address == None: raise ContractNotFound( "Must set Multicall address via `brownie.multicall(address=...)`" ) - elif not web3.eth.get_code(self._address, block_identifier=self._block_identifier): + elif not web3.eth.get_code(self.address, block_identifier=self.block_number): raise ContractNotFound( - f"Multicall at address {self._address} does not exist at block {self._block_identifier}" + f"Multicall at address {self.address} does not exist at block {self.block_number}" ) - self._contract = Contract.from_abi("Multicall", self._address, MULTICALL2_ABI) + self._contract = Contract.from_abi("Multicall", self.address, MULTICALL2_ABI) getattr(ContractCall, "__multicall")[get_ident()] = self def __exit__(self, exc_type: Exception, exc_val: Any, exc_tb: TracebackType) -> None: From 8686817a118f0395817558179507dc6a9eec2c47 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 14:21:54 -0400 Subject: [PATCH 31/34] docs: update with new usage --- docs/api-network.rst | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/docs/api-network.rst b/docs/api-network.rst index f0ca3b1cb..5cbf5e1a8 100644 --- a/docs/api-network.rst +++ b/docs/api-network.rst @@ -1667,6 +1667,10 @@ To implement a scaling strategy, subclass one of the above ABCs and implement th The ``multicall`` module contains the :func:`Multicall ` context manager, which allows for the batching of multiple constant contract function calls via ``Multicall2``. +.. note:: + + The :func:`Multicall ` context manager is not meant to be instantiated, and instead should be used via ``brownie.multicall`` + Multicall --------- @@ -1678,12 +1682,11 @@ Multicall Features: - 1. Uses a modified but familiar call API - 2. Lazy fetching of results - 3. Auto-deployment on development networks (on first use). - 4. Uses ``multicall2`` key in network-config as pre-defined multicall contract address - 5. Can specify/modify block number to make calls at particular block heights - 6. Calls which fail return ``None`` instad of causing all calls to fail + 1. Lazy fetching of results + 2. Auto-deployment on development networks (on first use). + 3. Uses ``multicall2`` key in network-config as pre-defined multicall contract address + 4. Can specify/modify block number to make calls at particular block heights + 5. Calls which fail return ``None`` instad of causing all calls to fail .. code-block:: python @@ -1691,10 +1694,10 @@ Multicall >>> from brownie import Contract >>> addr_provider = Contract("0x0000000022D53366457F9d5E68Ec105046FC4383") >>> registry = Contract(addr_provider.get_registry()) - >>> with brownie.Multicall() as m: - ... pool_count = registry.pool_count() # standard call, no batching - ... pools = [registry.pool_list(i, {"from": m}) for i in range(pool_count)] # batched - ... gauges = [registry.get_gauges(pool, {"from": m}) for pool in pools] # batched + >>> with brownie.multicall: + ... pool_count = registry.pool_count.call() # standard call, no batching + ... pools = [registry.pool_list(i) for i in range(pool_count)] # batched + ... gauges = [registry.get_gauges(pool) for pool in pools] # batched ... print(*zip(pools, gauges), sep="\n") Multicall Attributes @@ -1706,9 +1709,9 @@ Multicall Attributes .. code-block:: python - >>> Multicall().address + >>> brownie.multicall.address 0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696 - >>> Multicall("0xc8E51042792d7405184DfCa245F2d27B94D013b6").address + >>> brownie.multicall(address="0xc8E51042792d7405184DfCa245F2d27B94D013b6").address 0xc8E51042792d7405184DfCa245F2d27B94D013b6 .. py:attribute:: Multicall.block_number @@ -1719,14 +1722,10 @@ Multicall Attributes ``Multicall`` relies on an instance of ``Multicall2`` being available for aggregating results. If you set the block_height before the ``Multicall2`` instance you are using was deployed a ``ContractNotFound`` error will be raised. - .. note:: - - You can modify the block height used within the context manager, this will flush the queue of currently pending calls, and then adjust to the new block height - .. code-block:: python - >>> m = Multicall() - >>> m.block_number + >>> with brownie.multicall(block_identifier=12733683): + ... brownie.multicall.block_number 12733683 Multicall Methods @@ -1738,7 +1737,7 @@ Multicall Methods .. code-block:: python - >>> multicall2 = Multicall.deploy({"from": alice}) + >>> multicall2 = brownie.multicall.deploy({"from": alice}) .. py:classmethod:: Multicall.flush @@ -1748,10 +1747,10 @@ Multicall Methods >>> results = [] >>> long_list_of_addresses = [...] >>> token = Contract(...) - >>> with Multicall() as m: + >>> with brownie.multicall: ... for i, addr in enumerate(long_list_of_addresses): ... if i % 1_000: - ... m.flush() + ... brownie.multicall.flush() ... results.append(token.balanceOf(addr)) Multicall Internal Attributes From dd30cefa26cc5ce21198aeda56472b1c94c4f22c Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Tue, 13 Jul 2021 14:29:54 -0400 Subject: [PATCH 32/34] chore: linting --- brownie/network/multicall.py | 9 ++++----- tests/network/test_mulitcall.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index ce9c2121f..000a294e5 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -1,18 +1,17 @@ import json +from collections import defaultdict from dataclasses import dataclass +from threading import get_ident from types import FunctionType, TracebackType from typing import Any, Dict, List, Tuple, Union from lazy_object_proxy import Proxy -from toolz.itertoolz import get from wrapt import ObjectProxy -from brownie.network import accounts, web3 from brownie._config import BROWNIE_FOLDER, CONFIG from brownie.exceptions import ContractNotFound +from brownie.network import accounts, web3 from brownie.network.contract import Contract, ContractCall -from threading import get_ident -from collections import defaultdict from brownie.project import compile_source DATA_DIR = BROWNIE_FOLDER.joinpath("data") @@ -127,7 +126,7 @@ def __enter__(self) -> "Multicall": self.block_number = self.block_number or web3.eth.get_block_number() - if self.address == None: + if self.address is None: raise ContractNotFound( "Must set Multicall address via `brownie.multicall(address=...)`" ) diff --git a/tests/network/test_mulitcall.py b/tests/network/test_mulitcall.py index 2cf0448ec..3ea1cccf8 100644 --- a/tests/network/test_mulitcall.py +++ b/tests/network/test_mulitcall.py @@ -123,4 +123,4 @@ def test_all_values_come_from_the_same_block(chain, devnetwork): fourth_call = brownie.multicall._contract.getBlockNumber() assert first_call == second_call == third_call == fourth_call - assert brownie.multicall._contract.getBlockNumber() == first_call + 20 \ No newline at end of file + assert brownie.multicall._contract.getBlockNumber() == first_call + 20 From e85bf9f728ecf25821653ddc863c4b006b1f522d Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sat, 17 Jul 2021 22:06:36 -0400 Subject: [PATCH 33/34] chore: lint --- brownie/network/multicall.py | 14 +++++++------- tests/network/test_mulitcall.py | 2 -- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index 000a294e5..fc32ac342 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from threading import get_ident from types import FunctionType, TracebackType -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from lazy_object_proxy import Proxy from wrapt import ObjectProxy @@ -55,10 +55,10 @@ def __init__(self) -> None: ContractCall.__call__.__code__ = self._proxy_call.__code__ def __call__( - self, address: str = None, block_identifier: Union[str, bytes, int] = None + self, address: Optional[str] = None, block_identifier: Union[str, bytes, int, None] = None ) -> "Multicall": - self.address = address - self.block_number = block_identifier + self.address = address # type: ignore + self.block_number = block_identifier # type: ignore return self def _flush(self, future_result: Result = None) -> Any: @@ -67,7 +67,7 @@ def _flush(self, future_result: Result = None) -> Any: # or this result has already been retrieved return future_result ContractCall.__call__.__code__ = getattr(ContractCall, "__original_call_code") - results = self._contract.tryAggregate( + results = self._contract.tryAggregate( # type: ignore False, [_call.calldata for _call in self._pending_calls], block_identifier=self.block_number, @@ -121,8 +121,8 @@ def __enter__(self) -> "Multicall": self.address = active_network["multicall2"] elif "cmd" in active_network: deployment = self.deploy({"from": accounts[0]}) - self.address = deployment.address - self.block_number = deployment.tx.block_number + self.address = deployment.address # type: ignore + self.block_number = deployment.tx.block_number # type: ignore self.block_number = self.block_number or web3.eth.get_block_number() diff --git a/tests/network/test_mulitcall.py b/tests/network/test_mulitcall.py index 3ea1cccf8..081c6609e 100644 --- a/tests/network/test_mulitcall.py +++ b/tests/network/test_mulitcall.py @@ -1,10 +1,8 @@ import inspect -import pytest from lazy_object_proxy import Proxy import brownie -from brownie.exceptions import ContractNotFound def test_auto_deploy_on_testnet(config, devnetwork): From 9868a76059a80d7c3332f00df8963c8f2f654970 Mon Sep 17 00:00:00 2001 From: Edward Amor Date: Sun, 18 Jul 2021 09:47:12 -0400 Subject: [PATCH 34/34] fix: remove threading import in proxy fn --- brownie/network/contract.py | 1 + brownie/network/multicall.py | 8 +------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/brownie/network/contract.py b/brownie/network/contract.py index ba10ade4d..725810c63 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -8,6 +8,7 @@ from collections import defaultdict from pathlib import Path from textwrap import TextWrapper +from threading import get_ident # noqa from typing import Any, Dict, Iterator, List, Match, Optional, Set, Tuple, Union from urllib.parse import urlparse diff --git a/brownie/network/multicall.py b/brownie/network/multicall.py index fc32ac342..a65ae0ade 100644 --- a/brownie/network/multicall.py +++ b/brownie/network/multicall.py @@ -94,13 +94,7 @@ def _call_contract(self, call: ContractCall, *args: Tuple, **kwargs: Dict[str, A @staticmethod def _proxy_call(*args: Tuple, **kwargs: Dict[str, Any]) -> Any: - """Proxy code which substitutes `ContractCall.__call__` - - This makes constant contract calls look more like transactions since we require - users to specify a dictionary as the last argument with the from field - being the multicall2 instance being used.""" - from threading import get_ident - + """Proxy code which substitutes `ContractCall.__call__""" self = getattr(ContractCall, "__multicall", {}).get(get_ident()) if self: return self._call_contract(*args, **kwargs)