diff --git a/dank_mids/brownie_patch/account.py b/dank_mids/brownie_patch/account.py deleted file mode 100644 index 3f3c3f51..00000000 --- a/dank_mids/brownie_patch/account.py +++ /dev/null @@ -1,228 +0,0 @@ - -import sys -import threading -import time -from types import MethodType -from typing import Any, Dict, Optional, Tuple - -from brownie._config import CONFIG -from brownie.convert import Wei -from brownie.exceptions import VirtualMachineError -from brownie.network.account import Account -from brownie.network.rpc import Rpc -from brownie.network.state import Chain -from brownie.network.transaction import TransactionReceipt -from web3 import Web3 - -rpc = Rpc() - -def _patch_account(account: Account, w3: Web3) -> Account: - async def transfer_coro( - self, - to: Account = None, - amount: int = 0, - gas_limit: Optional[int] = None, - gas_buffer: Optional[float] = None, - gas_price: Optional[int] = None, - max_fee: Optional[int] = None, - priority_fee: Optional[int] = None, - data: str = None, - nonce: Optional[int] = None, - required_confs: int = 1, - allow_revert: bool = None, - silent: bool = None, - ) -> TransactionReceipt: - """ - Broadcast a transaction from this account. - Kwargs: - to: Account instance or address string to transfer to. - amount: Amount of ether to send, in wei. - gas_limit: Gas limit of the transaction. - gas_buffer: Multiplier to apply to gas limit. - gas_price: Gas price of legacy transaction. - max_fee: Max fee per gas of dynamic fee transaction. - priority_fee: Max priority fee per gas of dynamic fee transaction. - nonce: Nonce to use for the transaction. - data: Hexstring of data to include in transaction. - silent: Toggles console verbosity. - Returns: - TransactionReceipt object - """ - - receipt, exc = await self._make_transaction_coro( - to, - amount, - gas_limit, - gas_buffer, - gas_price, - max_fee, - priority_fee, - data or "", - nonce, - "", - required_confs, - allow_revert, - silent, - ) - - if rpc.is_active(): - undo_thread = threading.Thread( - target=Chain()._add_to_undo_buffer, - args=( - receipt, - self.transfer, - [], - { - "to": to, - "amount": amount, - "gas_limit": gas_limit, - "gas_buffer": gas_buffer, - "gas_price": gas_price, - "max_fee": max_fee, - "priority_fee": priority_fee, - "data": data, - }, - ), - daemon=True, - ) - undo_thread.start() - - receipt._raise_if_reverted(exc) - return receipt - - async def _make_transaction_coro( - self, - to: Optional["Account"], - amount: int, - gas_limit: Optional[int], - gas_buffer: Optional[float], - gas_price: Optional[int], - max_fee: Optional[int], - priority_fee: Optional[int], - data: str, - nonce: Optional[int], - fn_name: str, - required_confs: int, - allow_revert: Optional[bool], - silent: Optional[bool], - ) -> Tuple[TransactionReceipt, Optional[Exception]]: - # shared logic for `transfer` and `deploy` - if gas_limit and gas_buffer: - raise ValueError("Cannot set gas_limit and gas_buffer together") - if silent is None: - silent = bool(CONFIG.mode == "test" or CONFIG.argv["silent"]) - - if gas_price is None: - # if gas price is not explicitly set, load the default max fee and priority fee - if max_fee is None: - max_fee = CONFIG.active_network["settings"]["max_fee"] or None - if priority_fee is None: - priority_fee = CONFIG.active_network["settings"]["priority_fee"] or None - - if priority_fee == "auto": - priority_fee = Chain().priority_fee - - try: - # if max fee and priority fee are not set, use gas price - if max_fee is None and priority_fee is None: - gas_price, gas_strategy, gas_iter = self._gas_price(gas_price) - else: - gas_strategy, gas_iter = None, None - gas_limit = Wei(gas_limit) or self._gas_limit( - to, amount, gas_price or max_fee, gas_buffer, data - ) - except ValueError as e: - raise VirtualMachineError(e) from None - - with self._lock: - # we use a lock here to prevent nonce issues when sending many tx's at once - tx = { - "from": self.address, - "value": Wei(amount), - "nonce": nonce if nonce is not None else self._pending_nonce(), - "gas": web3.toHex(gas_limit), - "data": HexBytes(data), - } - if to: - tx["to"] = to_address(str(to)) - tx = _apply_fee_to_tx(tx, gas_price, max_fee, priority_fee) - txid = None - while True: - try: - response = await self._transact(tx, allow_revert) # type: ignore - exc, revert_data = None, None - if txid is None: - txid = HexBytes(response).hex() - if not silent: - print(f"\rTransaction sent: {color('bright blue')}{txid}{color}") - except ValueError as e: - if txid is None: - exc = VirtualMachineError(e) - if not hasattr(exc, "txid"): - raise exc from None - txid = exc.txid - print(f"\rTransaction sent: {color('bright blue')}{txid}{color}") - revert_data = (exc.revert_msg, exc.pc, exc.revert_type) - try: - receipt = TransactionReceipt( - txid, - self, - silent=silent, - required_confs=required_confs, - is_blocking=False, - name=fn_name, - revert_data=revert_data, - ) # type: ignore - break - except (TransactionNotFound, ValueError): - if not silent: - sys.stdout.write(f" Awaiting transaction in the mempool... {_marker[0]}\r") - sys.stdout.flush() - _marker.rotate(1) - time.sleep(1) - - receipt = self._await_confirmation(receipt, required_confs, gas_strategy, gas_iter) - if receipt.status != 1 and exc is None: - error_data = { - "message": ( - f"VM Exception while processing transaction: revert {receipt.revert_msg}" - ), - "code": -32000, - "data": { - receipt.txid: { - "error": "revert", - "program_counter": receipt._revert_pc, - "return": receipt.return_value, - "reason": receipt.revert_msg, - }, - }, - } - exc = VirtualMachineError(ValueError(error_data)) - - return receipt, exc - - async def _transact_coro(self, tx: Dict, allow_revert: bool) -> Any: - if allow_revert is None: - allow_revert = bool(CONFIG.network_type == "development") - if not allow_revert: - await self._check_for_revert_coro(tx) - return await w3.eth.send_transaction(tx) - - async def _check_for_revert_coro(self, tx: Dict) -> None: - try: - # remove gas price related values to avoid issues post-EIP1559 - # https://github.com/ethereum/go-ethereum/pull/23027 - skip_keys = {"gasPrice", "maxFeePerGas", "maxPriorityFeePerGas"} - await w3.eth.call({k: v for k, v in tx.items() if k not in skip_keys and v}) - except ValueError as exc: - msg = exc.args[0]["message"] if isinstance(exc.args[0], dict) else str(exc) - raise ValueError( - f"Execution reverted during call: '{msg}'. This transaction will likely revert. " - "If you wish to broadcast, include `allow_revert:True` as a transaction parameter.", - ) from None - - account.transfer_coro = MethodType(transfer_coro, account) - account._make_transaction_coro = MethodType(_make_transaction_coro, account) - account._transact_coro = MethodType(_transact_coro, account) - account._check_for_revert_coro = MethodType(_check_for_revert_coro, account) - return account diff --git a/dank_mids/brownie_patch/call.py b/dank_mids/brownie_patch/call.py index 3ed897fb..c61230a6 100644 --- a/dank_mids/brownie_patch/call.py +++ b/dank_mids/brownie_patch/call.py @@ -1,17 +1,58 @@ import functools from types import MethodType -from typing import Coroutine, Dict, Tuple, Union +from typing import Any, Coroutine, Dict, Tuple, Union import eth_abi +from brownie.convert.normalize import format_input, format_output +from brownie.convert.utils import get_type_strings from brownie.exceptions import VirtualMachineError -from brownie.network.contract import ContractCall, _get_tx +from brownie.network.contract import ContractCall from brownie.project.compiler.solidity import SOLIDITY_ERROR_CODES from hexbytes import HexBytes +from multicall.utils import run_in_subprocess from web3 import Web3 +def __encode_input(abi: Dict, signature: str, *args: Tuple) -> str: + data = format_input(abi, args) + types_list = get_type_strings(abi["inputs"]) + return signature + eth_abi.encode_abi(types_list, data).hex() + +def __decode_output(hexstr: str, abi: Dict) -> Any: + selector = HexBytes(hexstr)[:4].hex() + if selector == "0x08c379a0": + revert_str = eth_abi.decode_abi(["string"], HexBytes(hexstr)[4:])[0] + raise ValueError(f"Call reverted: {revert_str}") + elif selector == "0x4e487b71": + error_code = int(HexBytes(hexstr)[4:].hex(), 16) + if error_code in SOLIDITY_ERROR_CODES: + revert_str = SOLIDITY_ERROR_CODES[error_code] + else: + revert_str = f"Panic (error code: {error_code})" + raise ValueError(f"Call reverted: {revert_str}") + if abi["outputs"] and not hexstr: + raise ValueError("No data was returned - the call likely reverted") + + types_list = get_type_strings(abi["outputs"]) + result = eth_abi.decode_abi(types_list, HexBytes(hexstr)) + result = format_output(abi, result) + if len(result) == 1: + result = result[0] + return result + def _patch_call(call: ContractCall, w3: Web3) -> None: + async def _encode_input(self, *args): + return await run_in_subprocess( + __encode_input, + self.abi, + self.signature, + *(arg if not hasattr(arg, 'address') else arg.address for arg in args) + ) + + async def _decode_output(self, data): + return await run_in_subprocess(__decode_output, data, self.abi) + @functools.wraps(call) async def coroutine( self, @@ -19,32 +60,18 @@ async def coroutine( block_identifier: Union[int, str, bytes] = None, override: Dict = None ) -> Coroutine: - - args, tx = _get_tx(self._owner, args) - if tx["from"]: - tx["from"] = str(tx["from"]) - del tx["required_confs"] - tx.update({"to": self._address, "data": self.encode_input(*args)}) + if override: + raise ValueError("Cannot use state override with `coroutine`.") + + calldata = await self._encode_input(*args) try: - data = await w3.eth.call({k: v for k, v in tx.items() if v}, block_identifier, override) + data = await w3.eth.call({"to": self._address, "data": calldata}, block_identifier, override) except ValueError as e: raise VirtualMachineError(e) from None - selector = HexBytes(data)[:4].hex() - - if selector == "0x08c379a0": - revert_str = eth_abi.decode_abi(["string"], HexBytes(data)[4:])[0] - raise ValueError(f"Call reverted: {revert_str}") - elif selector == "0x4e487b71": - error_code = int(HexBytes(data)[4:].hex(), 16) - if error_code in SOLIDITY_ERROR_CODES: - revert_str = SOLIDITY_ERROR_CODES[error_code] - else: - revert_str = f"Panic (error code: {error_code})" - raise ValueError(f"Call reverted: {revert_str}") - if self.abi["outputs"] and not data: - raise ValueError("No data was returned - the call likely reverted") - return self.decode_output(data) + return await self._decode_output(data) call.coroutine = MethodType(coroutine, call) + call._encode_input = MethodType(_encode_input, call) + call._decode_output = MethodType(_decode_output, call) diff --git a/dank_mids/brownie_patch/contract.py b/dank_mids/brownie_patch/contract.py index 2970a571..5cb85762 100644 --- a/dank_mids/brownie_patch/contract.py +++ b/dank_mids/brownie_patch/contract.py @@ -5,7 +5,6 @@ from brownie.network.contract import ContractCall, ContractTx, OverloadedMethod from dank_mids.brownie_patch.call import _patch_call from dank_mids.brownie_patch.overloaded import _patch_overloaded_method -from dank_mids.brownie_patch.tx import _patch_tx from web3 import Web3 ContractMethod = Union[ContractCall, ContractTx, OverloadedMethod] @@ -13,9 +12,6 @@ def _patch_if_method(method: ContractMethod, w3: Web3): if isinstance(method, ContractCall) or isinstance(method, ContractTx): _patch_call(method, w3) - # TODO implement this properly - #elif isinstance(method, ContractTx): - # _patch_tx(method, w3) elif isinstance(method, OverloadedMethod): _patch_overloaded_method(method, w3) diff --git a/dank_mids/brownie_patch/overloaded.py b/dank_mids/brownie_patch/overloaded.py index 576bb313..3a606900 100644 --- a/dank_mids/brownie_patch/overloaded.py +++ b/dank_mids/brownie_patch/overloaded.py @@ -4,7 +4,6 @@ from brownie.network.contract import ContractCall, ContractTx, OverloadedMethod from dank_mids.brownie_patch.call import _patch_call -from dank_mids.brownie_patch.tx import _patch_tx from web3 import Web3 diff --git a/dank_mids/brownie_patch/tx.py b/dank_mids/brownie_patch/tx.py deleted file mode 100644 index 4cb6b6ac..00000000 --- a/dank_mids/brownie_patch/tx.py +++ /dev/null @@ -1,45 +0,0 @@ - -import functools -from types import MethodType -from typing import Coroutine, Dict, Tuple, Union - -from brownie.network.contract import ContractTx, _get_tx -from dank_mids.brownie_patch.account import _patch_account -from web3 import Web3 - - -def _patch_tx(call: ContractTx, w3: Web3) -> None: - - @functools.wraps(call) - async def coroutine( - self, - *args: Tuple, - block_identifier: Union[int, str, bytes] = None, - override: Dict = None - ) -> Coroutine: - - args, tx = _get_tx(self._owner, args) - if not tx["from"]: - raise AttributeError( - "Final argument must be a dict of transaction parameters that " - "includes a `from` field specifying the sender of the transaction" - ) - - if not hasattr(tx["from"], 'transfer_coro'): - tx["from"] = _patch_account(tx["from"], w3) - - return await tx["from"].transfer_coro( - self._address, - tx["value"], - gas_limit=tx["gas"], - gas_buffer=tx.get("gas_buffer"), - gas_price=tx.get("gas_price"), - max_fee=tx.get("max_fee"), - priority_fee=tx.get("priority_fee"), - nonce=tx["nonce"], - required_confs=tx["required_confs"], - data=self.encode_input(*args), - allow_revert=tx["allow_revert"], - ) - - call.coroutine = MethodType(coroutine, call) diff --git a/dank_mids/controller.py b/dank_mids/controller.py index a89aad69..7c3b6b01 100644 --- a/dank_mids/controller.py +++ b/dank_mids/controller.py @@ -80,18 +80,22 @@ async def __call__(self, params: Any) -> RPCResponse: @property def next_bid(self) -> int: + """ Returns the next unique batch id. """ return self._increment('bid') @property def next_mid(self) -> int: + """ Returns the next unique multicall id. """ return self._increment('mid') @property def next_cid(self) -> int: + """ Returns the next unique call id. """ return self._increment('cid') @sort_lazy_logger def should_batch(self, method: RPCEndpoint, params: Any) -> bool: + """ Determines whether or not a call should be passed to the DankMiddlewareController. """ if method != 'eth_call': sort_logger.debug(f"bypassed, method is {method}") return False @@ -101,6 +105,7 @@ def should_batch(self, method: RPCEndpoint, params: Any) -> bool: return True async def add_to_queue(self, params: Any) -> int: + """ Adds a call to the DankMiddlewareContoller's `pending_calls`. """ cid = self.next_cid block = params[1] while self._pools_closed: @@ -243,6 +248,7 @@ async def spoof_response(self, cid: int, block: str, params: List, data: Optiona async def _setup(self) -> None: if self._initializing: while self._initializing: + print('this line runs') await asyncio.sleep(0) return self._initializing = True diff --git a/requirements.txt b/requirements.txt index 7e5e3ad9..294d2a4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ bobs_lazy_logging>=0.0.2 +eth_retry>=0.1.1 multicall>=0.5.1 \ No newline at end of file diff --git a/tests/test_brownie_patch.py b/tests/test_brownie_patch.py index d32d852a..08bc0aea 100644 --- a/tests/test_brownie_patch.py +++ b/tests/test_brownie_patch.py @@ -1,12 +1,12 @@ from brownie import Contract from dank_mids.brownie_patch import patch_contract -from dank_mids.brownie_patch.contractcall import _patch_call -from dank_mids.brownie_patch.tx import _patch_tx +from dank_mids.brownie_patch.call import _patch_call from multicall.utils import await_awaitable from tests.fixtures import dank_w3 + def test_patch_call(): # must use from_explorer for gh testing workflow weth = Contract.from_explorer('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2') @@ -14,14 +14,6 @@ def test_patch_call(): assert hasattr(weth.totalSupply, 'coroutine') assert await_awaitable(weth.totalSupply.coroutine(block_identifier=13_000_000)) == 6620041514474872981393155 -# TODO implement this test once _patch_tx is implemented -#def test_patch_tx(): - # must use from_explorer for gh testing workflow -# uni_v3_quoter = Contract.from_explorer('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6') -# _patch_tx(uni_v3_quoter.quoteExactInput, dank_w3) -# assert hasattr(uni_v3_quoter.quoteExactInput, 'coroutine') -# assert await_awaitable(uni_v3_quoter.quoteExactInput.coroutine(block_identifier=13_000_000)) == 0 - def test_patch_contract(): # ContractCall # must use from_explorer for gh testing workflow @@ -30,8 +22,9 @@ def test_patch_contract(): assert await_awaitable(weth.totalSupply.coroutine(block_identifier=13_000_000)) == 6620041514474872981393155 # ContractTx - # TODO implement this part of the test once _patch_tx is implemented # must use from_explorer for gh testing workflow - #uni_v3_quoter = patch_contract(Contract.from_explorer('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'), dank_w3) - #assert hasattr(uni_v3_quoter.quoteExactInput, 'coroutine') - #assert await_awaitable(uni_v3_quoter.quoteExactInput.coroutine(block_identifier=13_000_000)) == 0 + uni_v3_quoter = patch_contract(Contract.from_explorer('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'), dank_w3) + assert hasattr(uni_v3_quoter.quoteExactInput, 'coroutine') + assert await_awaitable( + uni_v3_quoter.quoteExactInput.coroutine(b"\xc0*\xaa9\xb2#\xfe\x8d\n\x0e\\O'\xea\xd9\x08