diff --git a/ens/__init__.py b/ens/__init__.py index c3a463a5da..09c98f2922 100644 --- a/ens/__init__.py +++ b/ens/__init__.py @@ -2,6 +2,7 @@ from .main import ( ENS, + AsyncENS ) from .exceptions import ( diff --git a/ens/main.py b/ens/main.py index 928a3106a4..35e4ea733a 100644 --- a/ens/main.py +++ b/ens/main.py @@ -6,6 +6,7 @@ ) from typing import ( TYPE_CHECKING, + Awaitable, Optional, Sequence, Tuple, @@ -54,6 +55,7 @@ if TYPE_CHECKING: from web3 import Web3 # noqa: F401 from web3.contract import ( # noqa: F401 + AsyncContract, Contract, ) from web3.providers import ( # noqa: F401 @@ -65,16 +67,7 @@ ) -class ENS: - """ - Quick access to common Ethereum Name Service functions, - like getting the address for a name. - - Unless otherwise specified, all addresses are assumed to be a `str` in - `checksum format `_, - like: ``"0x314159265dD8dbb310642f98f50C066173C1259b"`` - """ - +class BaseENS: @staticmethod @wraps(label_to_hash) def labelhash(label: str) -> HexBytes: @@ -118,6 +111,34 @@ def __init__( self.ens = self.w3.eth.contract(abi=abis.ENS, address=ens_addr) self._resolverContract = self.w3.eth.contract(abi=abis.RESOLVER) + def _assert_control(self, account: ChecksumAddress, name: str, + parent_owned: Optional[str] = None) -> None: + if not address_in(account, self.w3.eth.accounts): + raise UnauthorizedError( + "in order to modify %r, you must control account %r, which owns %r" % ( + name, account, parent_owned or name + ) + ) + + +class ENS(BaseENS): + """ + Quick access to common Ethereum Name Service functions, + like getting the address for a name. + + Unless otherwise specified, all addresses are assumed to be a `str` in + `checksum format `_, + like: ``"0x314159265dD8dbb310642f98f50C066173C1259b"`` + """ + + def __init__( + self, + provider: 'BaseProvider' = cast('BaseProvider', default), + addr: ChecksumAddress = None, + middlewares: Optional[Sequence[Tuple['Middleware', str]]] = None, + ) -> None: + super().__init__(provider, addr, middlewares) + @classmethod def fromWeb3(cls, w3: 'Web3', addr: ChecksumAddress = None) -> 'ENS': """ @@ -331,15 +352,6 @@ def setup_owner( self._claim_ownership(new_owner, unowned, owned, super_owner, transact=transact) return new_owner - def _assert_control(self, account: ChecksumAddress, name: str, - parent_owned: Optional[str] = None) -> None: - if not address_in(account, self.w3.eth.accounts): - raise UnauthorizedError( - "in order to modify %r, you must control account %r, which owns %r" % ( - name, account, parent_owned or name - ) - ) - def _first_owner(self, name: str) -> Tuple[Optional[ChecksumAddress], Sequence[str], str]: """ Takes a name, and returns the owner of the deepest subdomain that has an owner @@ -411,3 +423,309 @@ def _setup_reverse( def _reverse_registrar(self) -> 'Contract': addr = self.ens.caller.owner(normal_name_to_hash(REVERSE_REGISTRAR_DOMAIN)) return self.w3.eth.contract(address=addr, abi=abis.REVERSE_REGISTRAR) + + +class AsyncENS(BaseENS): + """ + Quick access to common Ethereum Name Service functions, + like getting the address for a name. + + Unless otherwise specified, all addresses are assumed to be a `str` in + `checksum format `_, + like: ``"0x314159265dD8dbb310642f98f50C066173C1259b"`` + """ + + def __init__( + self, + provider: 'BaseProvider' = cast('BaseProvider', default), + addr: ChecksumAddress = None, + middlewares: Optional[Sequence[Tuple['Middleware', str]]] = None, + ) -> None: + super().__init__(provider, addr, middlewares) + + @classmethod + def fromWeb3(cls, w3: 'Web3', addr: ChecksumAddress = None) -> Awaitable['AsyncENS']: + """ + Generate an ENS instance with web3 + + :param `web3.Web3` web3: to infer connection information + :param hex-string addr: the address of the ENS registry on-chain. If not provided, + ENS.py will default to the mainnet ENS registry address. + """ + provider = w3.manager.provider + middlewares = w3.middleware_onion.middlewares + return cls(provider, addr=addr, middlewares=middlewares) + + async def address(self, name: str) -> Awaitable[Optional[ChecksumAddress]]: + """ + Look up the Ethereum address that `name` currently points to. + + :param str name: an ENS name to look up + :raises InvalidName: if `name` has invalid syntax + """ + return cast(ChecksumAddress, await self.resolve(name, 'addr')) + + async def name(self, address: ChecksumAddress) -> Awaitable[Optional[str]]: + """ + Look up the name that the address points to, using a + reverse lookup. Reverse lookup is opt-in for name owners. + + :param address: + :type address: hex-string + """ + reversed_domain = address_to_reverse_domain(address) + return await self.resolve(reversed_domain, get='name') + + async def setup_address( + self, + name: str, + address: Union[Address, ChecksumAddress, HexAddress] = cast(ChecksumAddress, default), + transact: Optional["TxParams"] = None + ) -> Awaitable[HexBytes]: + """ + Set up the name to point to the supplied address. + The sender of the transaction must own the name, or + its parent name. + + Example: If the caller owns ``parentname.eth`` with no subdomains + and calls this method with ``sub.parentname.eth``, + then ``sub`` will be created as part of this call. + + :param str name: ENS name to set up + :param str address: name will point to this address, in checksum format. If ``None``, + erase the record. If not specified, name will point to the owner's address. + :param dict transact: the transaction configuration, like in + :meth:`~web3.eth.Eth.send_transaction` + :raises InvalidName: if ``name`` has invalid syntax + :raises UnauthorizedError: if ``'from'`` in `transact` does not own `name` + """ + if not transact: + transact = {} + transact = deepcopy(transact) + owner = await self.setup_owner(name, transact=transact) + await self._assert_control(owner, name) + if is_none_or_zero_address(address): + address = None + elif address is default: + address = owner + elif is_binary_address(address): + address = to_checksum_address(cast(str, address)) + elif not is_checksum_address(address): + raise ValueError("You must supply the address in checksum format") + if await self.address(name) == address: # type: ignore + return None + if address is None: + address = EMPTY_ADDR_HEX + transact['from'] = owner + resolver: 'Contract' = await self._set_resolver(name, transact=transact) + return await resolver.functions.setAddr(raw_name_to_hash(name), address).transact(transact) + + async def setup_name( + self, + name: str, + address: Optional[ChecksumAddress] = None, + transact: Optional["TxParams"] = None + ) -> Awaitable[HexBytes]: + """ + Set up the address for reverse lookup, aka "caller ID". + After successful setup, the method :meth:`~ens.main.ENS.name` will return + `name` when supplied with `address`. + + :param str name: ENS name that address will point to + :param str address: to set up, in checksum format + :param dict transact: the transaction configuration, like in + :meth:`~web3.eth.send_transaction` + :raises AddressMismatch: if the name does not already point to the address + :raises InvalidName: if `name` has invalid syntax + :raises UnauthorizedError: if ``'from'`` in `transact` does not own `name` + :raises UnownedName: if no one owns `name` + """ + if not transact: + transact = {} + transact = deepcopy(transact) + if not name: + await self._assert_control(address, 'the reverse record') + return await self._setup_reverse(None, address, transact=transact) + else: + resolved = await self.address(name) + if is_none_or_zero_address(address): + address = resolved + elif resolved and address != resolved and resolved != EMPTY_ADDR_HEX: + raise AddressMismatch( + "Could not set address %r to point to name, because the name resolves to %r. " + "To change the name for an existing address, call setup_address() first." % ( + address, resolved + ) + ) + if is_none_or_zero_address(address): + address = await self.owner(name) + if is_none_or_zero_address(address): + raise UnownedName("claim subdomain using setup_address() first") + if is_binary_address(address): + address = to_checksum_address(address) + if not is_checksum_address(address): + raise ValueError("You must supply the address in checksum format") + await self._assert_control(address, name) + if not resolved: + await self.setup_address(name, address, transact=transact) + return await self._setup_reverse(name, address, transact=transact) + + async def resolve(self, name: str, + get: str = 'addr') -> Awaitable[Optional[Union[ChecksumAddress, str]]]: + normal_name = normalize_name(name) + resolver = self.resolver(normal_name) + if resolver: + lookup_function = getattr(resolver.functions, get) + namehash = normal_name_to_hash(normal_name) + address = await lookup_function(namehash).call() + if is_none_or_zero_address(address): + return None + return address + else: + return None + + async def resolver(self, normal_name: str) -> Awaitable[Optional['AsyncContract']]: + resolver_addr = await self.ens.caller.resolver(normal_name_to_hash(normal_name)) + if is_none_or_zero_address(resolver_addr): + return None + return self._resolverContract(address=resolver_addr) + + async def reverser(self, + target_address: ChecksumAddress) -> Awaitable[Optional['AsyncContract']]: + reversed_domain = address_to_reverse_domain(target_address) + return await self.resolver(reversed_domain) + + async def owner(self, name: str) -> ChecksumAddress: + """ + Get the owner of a name. Note that this may be different from the + deed holder in the '.eth' registrar. Learn more about the difference + between deed and name ownership in the ENS `Managing Ownership docs + `_ + + :param str name: ENS name to look up + :return: owner address + :rtype: str + """ + node = raw_name_to_hash(name) + return await self.ens.caller.owner(node) + + async def setup_owner( + self, + name: str, + new_owner: ChecksumAddress = cast(ChecksumAddress, default), + transact: Optional["TxParams"] = None + ) -> ChecksumAddress: + """ + Set the owner of the supplied name to `new_owner`. + + For typical scenarios, you'll never need to call this method directly, + simply call :meth:`setup_name` or :meth:`setup_address`. This method does *not* + set up the name to point to an address. + + If `new_owner` is not supplied, then this will assume you + want the same owner as the parent domain. + + If the caller owns ``parentname.eth`` with no subdomains + and calls this method with ``sub.parentname.eth``, + then ``sub`` will be created as part of this call. + + :param str name: ENS name to set up + :param new_owner: account that will own `name`. If ``None``, set owner to empty addr. + If not specified, name will point to the parent domain owner's address. + :param dict transact: the transaction configuration, like in + :meth:`~web3.eth.Eth.send_transaction` + :raises InvalidName: if `name` has invalid syntax + :raises UnauthorizedError: if ``'from'`` in `transact` does not own `name` + :returns: the new owner's address + """ + if not transact: + transact = {} + transact = deepcopy(transact) + (super_owner, unowned, owned) = await self._first_owner(name) + if new_owner is default: + new_owner = super_owner + elif not new_owner: + new_owner = ChecksumAddress(EMPTY_ADDR_HEX) + else: + new_owner = to_checksum_address(new_owner) + current_owner = await self.owner(name) + if new_owner == EMPTY_ADDR_HEX and not current_owner: + return None + elif current_owner == new_owner: + return current_owner + else: + await self._assert_control(super_owner, name, owned) + await self._claim_ownership(new_owner, unowned, owned, super_owner, transact=transact) + return new_owner + + async def _first_owner(self, name: str) -> Tuple[Optional[ChecksumAddress], Sequence[str], str]: + """ + Takes a name, and returns the owner of the deepest subdomain that has an owner + + :returns: (owner or None, list(unowned_subdomain_labels), first_owned_domain) + """ + owner = None + unowned = [] + pieces = normalize_name(name).split('.') + while pieces and is_none_or_zero_address(owner): + name = '.'.join(pieces) + owner = await self.owner(name) + if is_none_or_zero_address(owner): + unowned.append(pieces.pop(0)) + return (owner, unowned, name) + + async def _claim_ownership( + self, + owner: ChecksumAddress, + unowned: Sequence[str], + owned: str, + old_owner: Optional[ChecksumAddress] = None, + transact: Optional["TxParams"] = None + ) -> None: + if not transact: + transact = {} + transact = deepcopy(transact) + transact['from'] = old_owner or owner + for label in reversed(unowned): + await self.ens.functions.setSubnodeOwner( + raw_name_to_hash(owned), + label_to_hash(label), + owner + ).transact(transact) + owned = "%s.%s" % (label, owned) + + async def _set_resolver( + self, + name: str, + resolver_addr: Optional[ChecksumAddress] = None, + transact: Optional["TxParams"] = None + ) -> 'Contract': + if not transact: + transact = {} + transact = deepcopy(transact) + if is_none_or_zero_address(resolver_addr): + resolver_addr = await self.address('resolver.eth') + namehash = raw_name_to_hash(name) + if await self.ens.caller.resolver(namehash) != resolver_addr: + await self.ens.functions.setResolver( + namehash, + resolver_addr + ).transact(transact) + return await self._resolverContract(address=resolver_addr) + + async def _setup_reverse( + self, name: str, address: ChecksumAddress, transact: Optional["TxParams"] = None + ) -> HexBytes: + if not transact: + transact = {} + transact = deepcopy(transact) + if name: + name = normalize_name(name) + else: + name = '' + transact['from'] = address + return await self._reverse_registrar().functions.setName(name).transact(transact) + + async def _reverse_registrar(self) -> 'Contract': + addr = await self.ens.caller.owner(normal_name_to_hash(REVERSE_REGISTRAR_DOMAIN)) + return await self.w3.eth.contract(address=addr, abi=abis.REVERSE_REGISTRAR) diff --git a/newsfragments/2318.bugfix.rst b/newsfragments/2318.bugfix.rst new file mode 100644 index 0000000000..7b69a5600c --- /dev/null +++ b/newsfragments/2318.bugfix.rst @@ -0,0 +1 @@ +In ENS the contract function to resolve an ENS address was being called twice in error. One of those calls was removed. \ No newline at end of file diff --git a/tests/core/contracts/conftest.py b/tests/core/contracts/conftest.py index 2941572be8..29d1fbd4e0 100644 --- a/tests/core/contracts/conftest.py +++ b/tests/core/contracts/conftest.py @@ -5,7 +5,14 @@ from eth_utils import ( event_signature_to_log_topic, ) +from eth_utils.toolz import ( + identity, +) +from ens import ( + AsyncENS, +) +from web3 import Web3 from web3._utils.module_testing.emitter_contract import ( CONTRACT_EMITTER_ABI, CONTRACT_EMITTER_CODE, @@ -46,6 +53,15 @@ REVERT_CONTRACT_BYTECODE, REVERT_CONTRACT_RUNTIME_CODE, ) +from web3.contract import ( + AsyncContract, +) +from web3.eth import ( + AsyncEth, +) +from web3.providers.eth_tester.main import ( + AsyncEthereumTesterProvider, +) CONTRACT_NESTED_TUPLE_SOURCE = """ pragma solidity >=0.4.19 <0.6.0; @@ -1036,3 +1052,39 @@ def estimateGas(request): @pytest.fixture def buildTransaction(request): return functools.partial(invoke_contract, api_call_desig='buildTransaction') + + +async def async_deploy(web3, Contract, apply_func=identity, args=None): + args = args or [] + deploy_txn = await Contract.constructor(*args).transact() + deploy_receipt = await web3.eth.wait_for_transaction_receipt(deploy_txn) + assert deploy_receipt is not None + address = apply_func(deploy_receipt['contractAddress']) + contract = Contract(address=address) + assert contract.address == address + assert len(await web3.eth.get_code(contract.address)) > 0 + return contract + + +@pytest.fixture() +async def async_w3(): + provider = AsyncEthereumTesterProvider() + w3 = Web3(provider, modules={'eth': [AsyncEth]}, + middlewares=provider.middlewares) + w3.ens = AsyncENS.fromWeb3(w3) + w3.eth.default_account = await w3.eth.coinbase + return w3 + + +@pytest.fixture() +def AsyncMathContract(async_w3, MATH_ABI, MATH_CODE, MATH_RUNTIME): + contract = AsyncContract.factory(async_w3, + abi=MATH_ABI, + bytecode=MATH_CODE, + bytecode_runtime=MATH_RUNTIME) + return contract + + +@pytest.fixture() +async def async_math_contract(async_w3, AsyncMathContract, address_conversion_func): + return await async_deploy(async_w3, AsyncMathContract, address_conversion_func) diff --git a/tests/core/contracts/test_contract_call_interface.py b/tests/core/contracts/test_contract_call_interface.py index 6b1c1809b8..8946a290df 100644 --- a/tests/core/contracts/test_contract_call_interface.py +++ b/tests/core/contracts/test_contract_call_interface.py @@ -879,3 +879,9 @@ def test_call_revert_contract(revert_contract): # which does not contain the revert reason. Avoid that by giving a gas # value. revert_contract.functions.revertWithMessage().call({'gas': 100000}) + + +@pytest.mark.asyncio +async def test_async_call_no_args(async_math_contract): + result = await async_math_contract.functions.return13().call() + assert result == 13 diff --git a/web3/contract.py b/web3/contract.py index 38e98b4152..89765e2448 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -151,27 +151,33 @@ ACCEPTABLE_EMPTY_STRINGS = ["0x", b"0x", "", b""] -class ContractFunctions: +class BaseContractFunctions: """Class containing contract function objects """ - def __init__(self, abi: ABI, w3: 'Web3', address: Optional[ChecksumAddress] = None) -> None: + def __init__(self, + abi: ABI, + w3: 'Web3', + contract_function_class: Union[Type['ContractFunction'], + Type['AsyncContractFunction']], + address: Optional[ChecksumAddress] = None) -> None: + self.abi = abi self.w3 = w3 self.address = address if self.abi: self._functions = filter_by_type('function', self.abi) - for func in self._functions: - setattr( - self, + for func in self._functions: + setattr( + self, + func['name'], + contract_function_class.factory( func['name'], - ContractFunction.factory( - func['name'], - w3=self.w3, - contract_abi=self.abi, - address=self.address, - function_identifier=func['name'])) + w3=self.w3, + contract_abi=self.abi, + address=self.address, + function_identifier=func['name'])) def __iter__(self) -> Generator[str, None, None]: if not hasattr(self, '_functions') or not self._functions: @@ -208,6 +214,24 @@ def __hasattr__(self, event_name: str) -> bool: return False +class ContractFunctions(BaseContractFunctions): + def __init__(self, + abi: ABI, + w3: 'Web3', + address: Optional[ChecksumAddress] = None, + ) -> None: + super().__init__(abi, w3, ContractFunction, address) + + +class AsyncContractFunctions(BaseContractFunctions): + def __init__(self, + abi: ABI, + w3: 'Web3', + address: Optional[ChecksumAddress] = None, + ) -> None: + super().__init__(abi, w3, AsyncContractFunction, address) + + class ContractEvents: """Class containing contract event objects @@ -276,7 +300,7 @@ def __hasattr__(self, event_name: str) -> bool: return False -class Contract: +class BaseContract: """Base class for Contract proxy classes. First you need to create your Contract classes using @@ -326,45 +350,18 @@ class Contract: def __init__(self, address: Optional[ChecksumAddress] = None) -> None: """Create a new smart contract proxy object. - :param address: Contract address as 0x hex string - """ - if self.w3 is None: - raise AttributeError( - 'The `Contract` class has not been initialized. Please use the ' - '`web3.contract` interface to create your contract class.' - ) - - if address: - self.address = normalize_address(self.w3.ens, address) - - if not self.address: - raise TypeError("The address argument is required to instantiate a contract.") + :param address: Contract address as 0x hex string""" - self.functions = ContractFunctions(self.abi, self.w3, self.address) self.caller = ContractCaller(self.abi, self.w3, self.address) self.events = ContractEvents(self.abi, self.w3, self.address) self.fallback = Contract.get_fallback_function(self.abi, self.w3, self.address) self.receive = Contract.get_receive_function(self.abi, self.w3, self.address) @classmethod - def factory(cls, w3: 'Web3', class_name: Optional[str] = None, **kwargs: Any) -> 'Contract': - - kwargs['w3'] = w3 + def factory(cls, + contract: Union['Contract', 'AsyncContract'] + ) -> Union['Contract', 'AsyncContract']: - normalizers = { - 'abi': normalize_abi, - 'address': partial(normalize_address, kwargs['w3'].ens), - 'bytecode': normalize_bytecode, - 'bytecode_runtime': normalize_bytecode, - } - - contract = cast(Contract, PropertyCheckingFactory( - class_name or cls.__name__, - (cls,), - kwargs, - normalizers=normalizers, - )) - contract.functions = ContractFunctions(contract.abi, contract.w3) contract.caller = ContractCaller(contract.abi, contract.w3, contract.address) contract.events = ContractEvents(contract.abi, contract.w3) contract.fallback = Contract.get_fallback_function(contract.abi, contract.w3) @@ -583,6 +580,87 @@ def _encode_constructor_data(cls, args: Optional[Any] = None, return deploy_data +class Contract(BaseContract): + + def __init__(self, address: Optional[ChecksumAddress] = None) -> None: + if self.w3 is None: + raise AttributeError( + 'The `Contract` class has not been initialized. Please use the ' + '`web3.contract` interface to create your contract class.' + ) + + if address: + self.address = normalize_address(self.w3.ens, address) + + if not self.address: + raise TypeError("The address argument is required to instantiate a contract.") + + self.functions = ContractFunctions(self.abi, self.w3, self.address) + super().__init__(address) + + @classmethod + def factory(cls, w3: 'Web3', class_name: Optional[str] = None, **kwargs: Any) -> 'Contract': + kwargs['w3'] = w3 + + normalizers = { + 'abi': normalize_abi, + 'address': partial(normalize_address, kwargs['w3'].ens), + 'bytecode': normalize_bytecode, + 'bytecode_runtime': normalize_bytecode, + } + + contract = cast(Contract, PropertyCheckingFactory( + class_name or cls.__name__, + (cls,), + kwargs, + normalizers=normalizers, + )) + contract.functions = ContractFunctions(contract.abi, contract.w3) + contract = BaseContract.factory(contract) + return contract + + +class AsyncContract(BaseContract): + + def __init__(self, address: Optional[ChecksumAddress] = None) -> None: + if self.w3 is None: + raise AttributeError( + 'The `Contract` class has not been initialized. Please use the ' + '`web3.contract` interface to create your contract class.' + ) + + if address: + self.address = normalize_address(self.w3.ens, address) + + if not self.address: + raise TypeError("The address argument is required to instantiate a contract.") + self.functions = AsyncContractFunctions(self.abi, self.w3, self.address) + super().__init__(address) + + @classmethod + def factory(cls, w3: 'Web3', + class_name: Optional[str] = None, + **kwargs: Any) -> 'AsyncContract': + kwargs['w3'] = w3 + + normalizers = { + 'abi': normalize_abi, + 'address': partial(normalize_address, kwargs['w3'].ens), + 'bytecode': normalize_bytecode, + 'bytecode_runtime': normalize_bytecode, + } + + contract = cast(AsyncContract, PropertyCheckingFactory( + class_name or cls.__name__, + (cls,), + kwargs, + normalizers=normalizers, + )) + contract.functions = AsyncContractFunctions(contract.abi, contract.w3) + contract = BaseContract.factory(contract) + return contract + + def mk_collision_prop(fn_name: str) -> Callable[[], None]: def collision_fn() -> NoReturn: msg = "Namespace collision for function name {0} with ConciseContract API.".format(fn_name) @@ -591,7 +669,7 @@ def collision_fn() -> NoReturn: return collision_fn -class ContractConstructor: +class BaseContractConstructor: """ Class for contract constructor API. """ @@ -644,8 +722,7 @@ def estimateGas( estimate_gas_transaction, block_identifier=block_identifier ) - @combomethod - def transact(self, transaction: Optional[TxParams] = None) -> HexBytes: + def _get_transaction(self, transaction: Optional[TxParams] = None) -> HexBytes: if transaction is None: transact_transaction: TxParams = {} else: @@ -659,8 +736,7 @@ def transact(self, transaction: Optional[TxParams] = None) -> HexBytes: transact_transaction['data'] = self.data_in_transaction - # TODO: handle asynchronous contract creation - return self.w3.eth.send_transaction(transact_transaction) + return transact_transaction @combomethod def buildTransaction(self, transaction: Optional[TxParams] = None) -> TxParams: @@ -694,6 +770,20 @@ def check_forbidden_keys_in_transaction( ) +class ContractConstructor(BaseContractConstructor): + + @combomethod + def transact(self, transaction: Optional[TxParams] = None) -> HexBytes: + return self.w3.eth.send_transaction(self._get_transaction(transaction)) + + +class AsyncContractConstructor(BaseContractConstructor): + + @combomethod + async def transact(self, transaction: Optional[TxParams] = None) -> HexBytes: + return await self.w3.eth.send_transaction(self._get_transaction(transaction)) + + class ConciseMethod: ALLOWED_MODIFIERS = {'call', 'estimateGas', 'transact', 'buildTransaction'} @@ -844,7 +934,7 @@ def __getattr__(self, attr: Any) -> Callable[[], None]: return self._raise_exception -class ContractFunction: +class BaseContractFunction: """Base class for contract functions A function accessed via the api contract.functions.myMethod(*args, **kwargs) @@ -897,35 +987,8 @@ def _set_function_info(self) -> None: self.arguments = merge_args_and_kwargs(self.abi, self.args, self.kwargs) - def call( - self, transaction: Optional[TxParams] = None, - block_identifier: BlockIdentifier = 'latest', - state_override: Optional[CallOverrideParams] = None, - ) -> Any: - """ - Execute a contract function call using the `eth_call` interface. + def _get_call_txparams(self, transaction: Optional[TxParams] = None) -> TxParams: - This method prepares a ``Caller`` object that exposes the contract - functions and public variables as callable Python functions. - - Reading a public ``owner`` address variable example: - - .. code-block:: python - - ContractFactory = w3.eth.contract( - abi=wallet_contract_definition["abi"] - ) - - # Not a real contract address - contract = ContractFactory("0x2f70d3d26829e412A602E83FE8EeBF80255AEeA5") - - # Read "owner" public variable - addr = contract.functions.owner().call() - - :param transaction: Dictionary of transaction info for web3 interface - :return: ``Caller`` object that has contract public functions - and variables exposed as Python methods - """ if transaction is None: call_transaction: TxParams = {} else: @@ -952,21 +1015,7 @@ def call( "Please ensure that this contract instance has an address." ) - block_id = parse_block_identifier(self.w3, block_identifier) - - return call_contract_function( - self.w3, - self.address, - self._return_data_normalizers, - self.function_identifier, - call_transaction, - block_id, - self.contract_abi, - self.abi, - state_override, - *self.args, - **self.kwargs - ) + return call_transaction def transact(self, transaction: Optional[TxParams] = None) -> HexBytes: if transaction is None: @@ -1106,6 +1155,104 @@ def __repr__(self) -> str: return '' % self.fn_name +class ContractFunction(BaseContractFunction): + + def call(self, transaction: Optional[TxParams] = None, + block_identifier: BlockIdentifier = 'latest', + state_override: Optional[CallOverrideParams] = None, + ) -> Any: + """ + Execute a contract function call using the `eth_call` interface. + + This method prepares a ``Caller`` object that exposes the contract + functions and public variables as callable Python functions. + + Reading a public ``owner`` address variable example: + + .. code-block:: python + + ContractFactory = w3.eth.contract( + abi=wallet_contract_definition["abi"] + ) + + # Not a real contract address + contract = ContractFactory("0x2f70d3d26829e412A602E83FE8EeBF80255AEeA5") + + # Read "owner" public variable + addr = contract.functions.owner().call() + + :param transaction: Dictionary of transaction info for web3 interface + :return: ``Caller`` object that has contract public functions + and variables exposed as Python methods + """ + call_transaction = self._get_call_txparams(transaction) + + block_id = parse_block_identifier(self.w3, block_identifier) + + return call_contract_function(self.w3, + self.address, + self._return_data_normalizers, + self.function_identifier, + call_transaction, + block_id, + self.contract_abi, + self.abi, + state_override, + *self.args, + **self.kwargs + ) + + +class AsyncContractFunction(BaseContractFunction): + + async def call( + self, transaction: Optional[TxParams] = None, + block_identifier: BlockIdentifier = 'latest', + state_override: Optional[CallOverrideParams] = None, + ) -> Any: + """ + Execute a contract function call using the `eth_call` interface. + + This method prepares a ``Caller`` object that exposes the contract + functions and public variables as callable Python functions. + + Reading a public ``owner`` address variable example: + + .. code-block:: python + + ContractFactory = w3.eth.contract( + abi=wallet_contract_definition["abi"] + ) + + # Not a real contract address + contract = ContractFactory("0x2f70d3d26829e412A602E83FE8EeBF80255AEeA5") + + # Read "owner" public variable + addr = contract.functions.owner().call() + + :param transaction: Dictionary of transaction info for web3 interface + :return: ``Caller`` object that has contract public functions + and variables exposed as Python methods + """ + call_transaction = self._get_call_txparams(transaction) + + block_id = await async_parse_block_identifier(self.w3, block_identifier) + + return await async_call_contract_function( + self.w3, + self.address, + self._return_data_normalizers, + self.function_identifier, + call_transaction, + block_id, + self.contract_abi, + self.abi, + state_override, + *self.args, + **self.kwargs + ) + + class ContractEvent: """Base class for contract events @@ -1348,7 +1495,7 @@ def factory(cls, class_name: str, **kwargs: Any) -> PropertyCheckingFactory: return PropertyCheckingFactory(class_name, (cls,), kwargs) -class ContractCaller: +class BaseContractCaller: """ An alternative Contract API. @@ -1375,7 +1522,9 @@ def __init__(self, w3: 'Web3', address: ChecksumAddress, transaction: Optional[TxParams] = None, - block_identifier: BlockIdentifier = 'latest') -> None: + block_identifier: BlockIdentifier = 'latest', + contract_function_class: Optional[BaseContractFunction] = ContractFunction + ) -> None: self.w3 = w3 self.address = address self.abi = abi @@ -1387,7 +1536,7 @@ def __init__(self, self._functions = filter_by_type('function', self.abi) for func in self._functions: - fn: ContractFunction = ContractFunction.factory( + fn: BaseContractFunction = contract_function_class.factory( func['name'], w3=self.w3, contract_abi=self.abi, @@ -1453,6 +1602,29 @@ def call_function( return fn(*args, **kwargs).call(transaction, block_identifier) +class ContractCaller(BaseContractCaller): + def __init__(self, + abi: ABI, + w3: 'Web3', + address: ChecksumAddress, + transaction: Optional[TxParams] = None, + block_identifier: BlockIdentifier = 'latest') -> None: + super().__init__(abi, w3, address, + transaction, block_identifier, ContractFunction) + + +class AsyncContractCaller(BaseContractCaller): + + def __init__(self, + abi: ABI, + w3: 'Web3', + address: ChecksumAddress, + transaction: Optional[TxParams] = None, + block_identifier: BlockIdentifier = 'latest') -> None: + super().__init__(abi, w3, address, + transaction, block_identifier, AsyncContractFunction) + + def check_for_forbidden_api_filter_arguments( event_abi: ABIEvent, _filters: Dict[str, Any] ) -> None: @@ -1471,39 +1643,19 @@ def check_for_forbidden_api_filter_arguments( "method.") -def call_contract_function( - w3: 'Web3', - address: ChecksumAddress, - normalizers: Tuple[Callable[..., Any], ...], - function_identifier: FunctionIdentifier, - transaction: TxParams, - block_id: Optional[BlockIdentifier] = None, - contract_abi: Optional[ABI] = None, - fn_abi: Optional[ABIFunction] = None, - state_override: Optional[CallOverrideParams] = None, - *args: Any, - **kwargs: Any) -> Any: +def _call_contract_function(w3: 'Web3', + address: ChecksumAddress, + normalizers: Tuple[Callable[..., Any], ...], + function_identifier: FunctionIdentifier, + return_data: Union[bytes, bytearray], + contract_abi: Optional[ABI] = None, + fn_abi: Optional[ABIFunction] = None, + *args: Any, + **kwargs: Any) -> Any: """ Helper function for interacting with a contract function using the `eth_call` API. """ - call_transaction = prepare_transaction( - address, - w3, - fn_identifier=function_identifier, - contract_abi=contract_abi, - fn_abi=fn_abi, - transaction=transaction, - fn_args=args, - fn_kwargs=kwargs, - ) - - return_data = w3.eth.call( - call_transaction, - block_identifier=block_id, - state_override=state_override, - ) - if fn_abi is None: fn_abi = find_matching_fn_abi(contract_abi, w3.codec, function_identifier, args, kwargs) @@ -1541,6 +1693,94 @@ def call_contract_function( return normalized_data +def call_contract_function( + w3: 'Web3', + address: ChecksumAddress, + normalizers: Tuple[Callable[..., Any], ...], + function_identifier: FunctionIdentifier, + transaction: TxParams, + block_id: Optional[BlockIdentifier] = None, + contract_abi: Optional[ABI] = None, + fn_abi: Optional[ABIFunction] = None, + state_override: Optional[CallOverrideParams] = None, + *args: Any, + **kwargs: Any) -> Any: + """ + Helper function for interacting with a contract function using the + `eth_call` API. + """ + call_transaction = prepare_transaction( + address, + w3, + fn_identifier=function_identifier, + contract_abi=contract_abi, + fn_abi=fn_abi, + transaction=transaction, + fn_args=args, + fn_kwargs=kwargs, + ) + + return_data = w3.eth.call( + call_transaction, + block_identifier=block_id, + state_override=state_override, + ) + + return _call_contract_function(w3, + address, + normalizers, + function_identifier, + return_data, + contract_abi, + fn_abi, + args, + kwargs) + + +async def async_call_contract_function( + w3: 'Web3', + address: ChecksumAddress, + normalizers: Tuple[Callable[..., Any], ...], + function_identifier: FunctionIdentifier, + transaction: TxParams, + block_id: Optional[BlockIdentifier] = None, + contract_abi: Optional[ABI] = None, + fn_abi: Optional[ABIFunction] = None, + state_override: Optional[CallOverrideParams] = None, + *args: Any, + **kwargs: Any) -> Any: + """ + Helper function for interacting with a contract function using the + `eth_call` API. + """ + call_transaction = prepare_transaction( + address, + w3, + fn_identifier=function_identifier, + contract_abi=contract_abi, + fn_abi=fn_abi, + transaction=transaction, + fn_args=args, + fn_kwargs=kwargs, + ) + + return_data = await w3.eth.call( + call_transaction, + block_identifier=block_id, + state_override=state_override, + ) + + return _call_contract_function(w3, + address, + normalizers, + function_identifier, + return_data, + contract_abi, + fn_abi, + args, + kwargs) + + def parse_block_identifier(w3: 'Web3', block_identifier: BlockIdentifier) -> BlockIdentifier: if isinstance(block_identifier, int): return parse_block_identifier_int(w3, block_identifier) @@ -1552,6 +1792,19 @@ def parse_block_identifier(w3: 'Web3', block_identifier: BlockIdentifier) -> Blo raise BlockNumberOutofRange +async def async_parse_block_identifier(w3: 'Web3', + block_identifier: BlockIdentifier + ) -> BlockIdentifier: + if isinstance(block_identifier, int): + return parse_block_identifier_int(w3, block_identifier) + elif block_identifier in ['latest', 'earliest', 'pending']: + return block_identifier + elif isinstance(block_identifier, bytes) or is_hex_encoded_block_hash(block_identifier): + return await w3.eth.get_block(block_identifier)['number'] + else: + raise BlockNumberOutofRange + + def parse_block_identifier_int(w3: 'Web3', block_identifier_int: int) -> BlockNumber: if block_identifier_int >= 0: block_num = block_identifier_int diff --git a/web3/main.py b/web3/main.py index acbac1b9a3..c7ebdd125b 100644 --- a/web3/main.py +++ b/web3/main.py @@ -46,6 +46,7 @@ ) from ens import ENS +from ens.main import AsyncENS, BaseENS from web3._utils.abi import ( build_default_registry, build_strict_registry, @@ -238,7 +239,7 @@ def __init__( middlewares: Optional[Sequence[Any]] = None, modules: Optional[Dict[str, Union[Type[Module], Sequence[Any]]]] = None, external_modules: Optional[Dict[str, Union[Type[Module], Sequence[Any]]]] = None, - ens: ENS = cast(ENS, empty) + ens: Union[ENS, AsyncENS] = cast(ENS, empty) ) -> None: self.manager = self.RequestManager(self, provider, middlewares) # this codec gets used in the module initialization, @@ -346,14 +347,14 @@ def is_encodable(self, _type: TypeStr, value: Any) -> bool: return self.codec.is_encodable(_type, value) @property - def ens(self) -> ENS: + def ens(self) -> Union['ENS', 'AsyncENS']: if self._ens is cast(ENS, empty): return ENS.fromWeb3(self) else: return self._ens @ens.setter - def ens(self, new_ens: ENS) -> None: + def ens(self, new_ens: Union['ENS', 'AsyncENS']) -> None: self._ens = new_ens @property diff --git a/web3/providers/eth_tester/main.py b/web3/providers/eth_tester/main.py index e45df5082b..987b6e0000 100644 --- a/web3/providers/eth_tester/main.py +++ b/web3/providers/eth_tester/main.py @@ -17,18 +17,26 @@ from web3._utils.compat import ( Literal, ) +from web3.middleware.buffered_gas_estimate import ( + async_buffered_gas_estimate_middleware, +) from web3.providers import ( BaseProvider, ) from web3.providers.async_base import ( AsyncBaseProvider, ) +from web3.providers.eth_tester.defaults import ( + API_ENDPOINTS, +) from web3.types import ( RPCEndpoint, RPCResponse, ) from .middleware import ( + async_default_transaction_fields_middleware, + async_ethereum_tester_middleware, default_transaction_fields_middleware, ethereum_tester_middleware, ) @@ -43,13 +51,45 @@ class AsyncEthereumTesterProvider(AsyncBaseProvider): + middlewares = ( + async_buffered_gas_estimate_middleware, + async_default_transaction_fields_middleware, + async_ethereum_tester_middleware + ) + def __init__(self) -> None: - self.eth_tester = EthereumTesterProvider() + from eth_tester import EthereumTester + self.ethereum_tester = EthereumTester() + self.api_endpoints = API_ENDPOINTS - async def make_request( - self, method: RPCEndpoint, params: Any - ) -> RPCResponse: - return self.eth_tester.make_request(method, params) + async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse: + namespace, _, endpoint = method.partition('_') + from eth_tester.exceptions import TransactionFailed + try: + delegator = self.api_endpoints[namespace][endpoint] + except KeyError: + return RPCResponse( + {"error": f"Unknown RPC Endpoint: {method}"} + ) + try: + response = delegator(self.ethereum_tester, params) + except NotImplementedError: + return RPCResponse( + {"error": f"RPC Endpoint has not been implemented: {method}"} + ) + except TransactionFailed as e: + try: + reason = decode_single('(string)', e.args[0].args[0][4:])[0] + except (InsufficientDataBytes, AttributeError): + reason = e.args[0] + raise TransactionFailed(f'execution reverted: {reason}') + else: + return { + 'result': response, + } + + async def isConnected(self) -> Literal[True]: + return True class EthereumTesterProvider(BaseProvider): diff --git a/web3/providers/eth_tester/middleware.py b/web3/providers/eth_tester/middleware.py index 18fbd42965..eea53e14e6 100644 --- a/web3/providers/eth_tester/middleware.py +++ b/web3/providers/eth_tester/middleware.py @@ -39,7 +39,11 @@ from web3.middleware import ( construct_formatting_middleware, ) +from web3.middleware.formatting import ( + async_construct_formatting_middleware, +) from web3.types import ( + Middleware, RPCEndpoint, RPCResponse, TxParams, @@ -181,109 +185,120 @@ def is_hexstr(value: Any) -> bool: } receipt_result_formatter = apply_formatters_to_dict(RECEIPT_RESULT_FORMATTERS) +request_formatters = { + # Eth + RPCEndpoint('eth_getBlockByNumber'): apply_formatters_to_args( + apply_formatter_if(is_not_named_block, to_integer_if_hex), + ), + RPCEndpoint('eth_getFilterChanges'): apply_formatters_to_args(hex_to_integer), + RPCEndpoint('eth_getFilterLogs'): apply_formatters_to_args(hex_to_integer), + RPCEndpoint('eth_getBlockTransactionCountByNumber'): apply_formatters_to_args( + apply_formatter_if(is_not_named_block, to_integer_if_hex), + ), + RPCEndpoint('eth_getUncleCountByBlockNumber'): apply_formatters_to_args( + apply_formatter_if(is_not_named_block, to_integer_if_hex), + ), + RPCEndpoint('eth_getTransactionByBlockHashAndIndex'): apply_formatters_to_args( + identity, + to_integer_if_hex, + ), + RPCEndpoint('eth_getTransactionByBlockNumberAndIndex'): apply_formatters_to_args( + apply_formatter_if(is_not_named_block, to_integer_if_hex), + to_integer_if_hex, + ), + RPCEndpoint('eth_getUncleByBlockNumberAndIndex'): apply_formatters_to_args( + apply_formatter_if(is_not_named_block, to_integer_if_hex), + to_integer_if_hex, + ), + RPCEndpoint('eth_newFilter'): apply_formatters_to_args( + filter_request_transformer, + ), + RPCEndpoint('eth_getLogs'): apply_formatters_to_args( + filter_request_transformer, + ), + RPCEndpoint('eth_sendTransaction'): apply_formatters_to_args( + transaction_request_transformer, + ), + RPCEndpoint('eth_estimateGas'): apply_formatters_to_args( + transaction_request_transformer, + ), + RPCEndpoint('eth_call'): apply_formatters_to_args( + transaction_request_transformer, + apply_formatter_if(is_not_named_block, to_integer_if_hex), + ), + RPCEndpoint('eth_uninstallFilter'): apply_formatters_to_args(hex_to_integer), + RPCEndpoint('eth_getCode'): apply_formatters_to_args( + identity, + apply_formatter_if(is_not_named_block, to_integer_if_hex), + ), + RPCEndpoint('eth_getBalance'): apply_formatters_to_args( + identity, + apply_formatter_if(is_not_named_block, to_integer_if_hex), + ), + # EVM + RPCEndpoint('evm_revert'): apply_formatters_to_args(hex_to_integer), + # Personal + RPCEndpoint('personal_sendTransaction'): apply_formatters_to_args( + transaction_request_transformer, + identity, + ), +} +result_formatters = { + RPCEndpoint('eth_getBlockByHash'): apply_formatter_if( + is_dict, + block_result_remapper, + ), + RPCEndpoint('eth_getBlockByNumber'): apply_formatter_if( + is_dict, + block_result_remapper, + ), + RPCEndpoint('eth_getBlockTransactionCountByHash'): apply_formatter_if( + is_dict, + transaction_result_remapper, + ), + RPCEndpoint('eth_getBlockTransactionCountByNumber'): apply_formatter_if( + is_dict, + transaction_result_remapper, + ), + RPCEndpoint('eth_getTransactionByHash'): apply_formatter_if( + is_dict, + compose(transaction_result_remapper, transaction_result_formatter), + ), + RPCEndpoint('eth_getTransactionReceipt'): apply_formatter_if( + is_dict, + compose(receipt_result_remapper, receipt_result_formatter), + ), + RPCEndpoint('eth_newFilter'): integer_to_hex, + RPCEndpoint('eth_newBlockFilter'): integer_to_hex, + RPCEndpoint('eth_newPendingTransactionFilter'): integer_to_hex, + RPCEndpoint('eth_getLogs'): apply_formatter_if( + is_array_of_dicts, + apply_formatter_to_array(log_result_remapper), + ), + RPCEndpoint('eth_getFilterChanges'): apply_formatter_if( + is_array_of_dicts, + apply_formatter_to_array(log_result_remapper), + ), + RPCEndpoint('eth_getFilterLogs'): apply_formatter_if( + is_array_of_dicts, + apply_formatter_to_array(log_result_remapper), + ), + # EVM + RPCEndpoint('evm_snapshot'): integer_to_hex, +} + + +async def async_ethereum_tester_middleware(make_request, web3: "Web3") -> Middleware: + middleware = await async_construct_formatting_middleware( + request_formatters=request_formatters, + result_formatters=result_formatters + ) + return await middleware(make_request, web3) + ethereum_tester_middleware = construct_formatting_middleware( - request_formatters={ - # Eth - RPCEndpoint('eth_getBlockByNumber'): apply_formatters_to_args( - apply_formatter_if(is_not_named_block, to_integer_if_hex), - ), - RPCEndpoint('eth_getFilterChanges'): apply_formatters_to_args(hex_to_integer), - RPCEndpoint('eth_getFilterLogs'): apply_formatters_to_args(hex_to_integer), - RPCEndpoint('eth_getBlockTransactionCountByNumber'): apply_formatters_to_args( - apply_formatter_if(is_not_named_block, to_integer_if_hex), - ), - RPCEndpoint('eth_getUncleCountByBlockNumber'): apply_formatters_to_args( - apply_formatter_if(is_not_named_block, to_integer_if_hex), - ), - RPCEndpoint('eth_getTransactionByBlockHashAndIndex'): apply_formatters_to_args( - identity, - to_integer_if_hex, - ), - RPCEndpoint('eth_getTransactionByBlockNumberAndIndex'): apply_formatters_to_args( - apply_formatter_if(is_not_named_block, to_integer_if_hex), - to_integer_if_hex, - ), - RPCEndpoint('eth_getUncleByBlockNumberAndIndex'): apply_formatters_to_args( - apply_formatter_if(is_not_named_block, to_integer_if_hex), - to_integer_if_hex, - ), - RPCEndpoint('eth_newFilter'): apply_formatters_to_args( - filter_request_transformer, - ), - RPCEndpoint('eth_getLogs'): apply_formatters_to_args( - filter_request_transformer, - ), - RPCEndpoint('eth_sendTransaction'): apply_formatters_to_args( - transaction_request_transformer, - ), - RPCEndpoint('eth_estimateGas'): apply_formatters_to_args( - transaction_request_transformer, - ), - RPCEndpoint('eth_call'): apply_formatters_to_args( - transaction_request_transformer, - apply_formatter_if(is_not_named_block, to_integer_if_hex), - ), - RPCEndpoint('eth_uninstallFilter'): apply_formatters_to_args(hex_to_integer), - RPCEndpoint('eth_getCode'): apply_formatters_to_args( - identity, - apply_formatter_if(is_not_named_block, to_integer_if_hex), - ), - RPCEndpoint('eth_getBalance'): apply_formatters_to_args( - identity, - apply_formatter_if(is_not_named_block, to_integer_if_hex), - ), - # EVM - RPCEndpoint('evm_revert'): apply_formatters_to_args(hex_to_integer), - # Personal - RPCEndpoint('personal_sendTransaction'): apply_formatters_to_args( - transaction_request_transformer, - identity, - ), - }, - result_formatters={ - RPCEndpoint('eth_getBlockByHash'): apply_formatter_if( - is_dict, - block_result_remapper, - ), - RPCEndpoint('eth_getBlockByNumber'): apply_formatter_if( - is_dict, - block_result_remapper, - ), - RPCEndpoint('eth_getBlockTransactionCountByHash'): apply_formatter_if( - is_dict, - transaction_result_remapper, - ), - RPCEndpoint('eth_getBlockTransactionCountByNumber'): apply_formatter_if( - is_dict, - transaction_result_remapper, - ), - RPCEndpoint('eth_getTransactionByHash'): apply_formatter_if( - is_dict, - compose(transaction_result_remapper, transaction_result_formatter), - ), - RPCEndpoint('eth_getTransactionReceipt'): apply_formatter_if( - is_dict, - compose(receipt_result_remapper, receipt_result_formatter), - ), - RPCEndpoint('eth_newFilter'): integer_to_hex, - RPCEndpoint('eth_newBlockFilter'): integer_to_hex, - RPCEndpoint('eth_newPendingTransactionFilter'): integer_to_hex, - RPCEndpoint('eth_getLogs'): apply_formatter_if( - is_array_of_dicts, - apply_formatter_to_array(log_result_remapper), - ), - RPCEndpoint('eth_getFilterChanges'): apply_formatter_if( - is_array_of_dicts, - apply_formatter_to_array(log_result_remapper), - ), - RPCEndpoint('eth_getFilterLogs'): apply_formatter_if( - is_array_of_dicts, - apply_formatter_to_array(log_result_remapper), - ), - # EVM - RPCEndpoint('evm_snapshot'): integer_to_hex, - }, + request_formatters=request_formatters, + result_formatters=result_formatters ) @@ -313,6 +328,17 @@ def fill_default( return assoc(transaction, field, guess_val) +async def async_fill_default( + field: str, guess_func: Callable[..., Any], web3: "Web3", transaction: TxParams +) -> TxParams: + # type ignored b/c TxParams keys must be string literal types + if field in transaction and transaction[field] is not None: # type: ignore + return transaction + else: + guess_val = guess_func(web3, transaction) + return assoc(transaction, field, guess_val) + + def default_transaction_fields_middleware( make_request: Callable[[RPCEndpoint, Any], Any], w3: "Web3" ) -> Callable[[RPCEndpoint, Any], RPCResponse]: @@ -332,3 +358,19 @@ def middleware(method: RPCEndpoint, params: Any) -> RPCResponse: else: return make_request(method, params) return middleware + + +async def async_default_transaction_fields_middleware( + make_request: Callable[[RPCEndpoint, Any], Any], web3: "Web3" +) -> Callable[[RPCEndpoint, Any], RPCResponse]: + async def middleware(method: RPCEndpoint, params: Any) -> RPCResponse: + if method in ( + 'eth_call', + 'eth_estimateGas', + 'eth_sendTransaction', + ): + filled_transaction = await async_fill_default('from', guess_from, web3, params[0]) + return await make_request(method, [filled_transaction] + list(params)[1:]) # type: ignore + else: + return await make_request(method, params) + return middleware