diff --git a/ens/utils.py b/ens/utils.py index 08eb211b94..652e4788f8 100644 --- a/ens/utils.py +++ b/ens/utils.py @@ -212,3 +212,13 @@ def assert_signer_in_modifier_kwargs(modifier_kwargs: Any) -> ChecksumAddress: def is_none_or_zero_address(addr: Union[Address, ChecksumAddress, HexAddress]) -> bool: return not addr or addr == EMPTY_ADDR_HEX + + +def is_valid_ens_name(ens_name: str) -> bool: + split_domain = ens_name.split('.') + if len(split_domain) == 1: + return False + for name in split_domain: + if not is_valid_name(name): + return False + return True diff --git a/tests/core/eth-module/test_transactions.py b/tests/core/eth-module/test_transactions.py index f1728056b3..e7d21992d7 100644 --- a/tests/core/eth-module/test_transactions.py +++ b/tests/core/eth-module/test_transactions.py @@ -1,6 +1,10 @@ import pytest +from web3._utils.ens import ( + ens_addresses, +) from web3.exceptions import ( + NameNotFound, TimeExhausted, TransactionNotFound, ValidationError, @@ -41,6 +45,49 @@ def test_send_transaction_with_valid_chain_id(web3, make_chain_id, expect_succes assert 'chain ID' in str(exc_info.value) +@pytest.mark.parametrize( + 'to, _from', + ( + ( + 'registered-name-1.eth', + 'not-a-registered-name.eth', + ), + ( + 'not-a-registered-name.eth', + 'registered-name-1.eth', + ), + ) +) +def test_send_transaction_with_invalid_ens_names(web3, to, _from): + with ens_addresses(web3, [ + ('registered-name-1.eth', web3.eth.accounts[1]), + ]): + transaction = { + 'to': to, + 'chainId': web3.eth.chainId, + 'from': _from, + } + + with pytest.raises(NameNotFound): + web3.eth.sendTransaction(transaction) + + +def test_send_transaction_with_ens_names(web3): + with ens_addresses(web3, [ + ('registered-name-1.eth', web3.eth.accounts[1]), + ('registered-name-2.eth', web3.eth.accounts[0]) + ]): + transaction = { + 'to': 'registered-name-1.eth', + 'chainId': web3.eth.chainId, + 'from': 'registered-name-2.eth', + } + + txn_hash = web3.eth.sendTransaction(transaction) + receipt = web3.eth.waitForTransactionReceipt(txn_hash, timeout=RECEIPT_TIMEOUT) + assert receipt.get('blockNumber') is not None + + def test_wait_for_missing_receipt(web3): with pytest.raises(TimeExhausted): web3.eth.waitForTransactionReceipt(b'\0' * 32, timeout=RECEIPT_TIMEOUT) diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py index 4e867569f2..b7ad3524ec 100644 --- a/tests/integration/test_ethereum_tester.py +++ b/tests/integration/test_ethereum_tester.py @@ -215,8 +215,14 @@ def func_wrapper(self, eth_tester, *args, **kwargs): class TestEthereumTesterEthModule(EthModuleTest): test_eth_sign = not_implemented(EthModuleTest.test_eth_sign, ValueError) + test_eth_sign_ens_names = not_implemented( + EthModuleTest.test_eth_sign_ens_names, ValueError + ) test_eth_signTypedData = not_implemented(EthModuleTest.test_eth_signTypedData, ValueError) test_eth_signTransaction = not_implemented(EthModuleTest.test_eth_signTransaction, ValueError) + test_eth_signTransaction_ens_names = not_implemented( + EthModuleTest.test_eth_signTransaction_ens_names, ValueError + ) test_eth_submitHashrate = not_implemented(EthModuleTest.test_eth_submitHashrate, ValueError) test_eth_submitWork = not_implemented(EthModuleTest.test_eth_submitWork, ValueError) @@ -283,6 +289,10 @@ def test_eth_call_old_contract_state(self, eth_tester, web3, math_contract, unlo def test_eth_getStorageAt(self, web3, emitter_contract_address): super().test_eth_getStorageAt(web3, emitter_contract_address) + @pytest.mark.xfail(reason='json-rpc method is not implemented on eth-tester') + def test_eth_getStorageAt_ens_name(self, web3, emitter_contract_address): + super().test_eth_getStorageAt_ens_name(web3, emitter_contract_address) + def test_eth_estimateGas_with_block(self, web3, unlocked_account_dual_type): diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py index 22043ba607..709d76846a 100644 --- a/web3/_utils/module_testing/eth_module.py +++ b/web3/_utils/module_testing/eth_module.py @@ -30,9 +30,13 @@ HexBytes, ) +from web3._utils.ens import ( + ens_addresses, +) from web3.exceptions import ( BlockNotFound, InvalidAddress, + NameNotFound, TransactionNotFound, ) from web3.types import ( # noqa: F401 @@ -136,12 +140,35 @@ def test_eth_getBalance_with_block_identifier(self, web3: "Web3") -> None: assert is_integer(later_balance) assert later_balance > genesis_balance + @pytest.mark.parametrize('address, expect_success', [ + ('test-address.eth', True), + ('not-an-address.eth', False) + ]) + def test_eth_getBalance_with_ens_name( + self, web3: "Web3", address: ChecksumAddress, expect_success: bool + ) -> None: + with ens_addresses(web3, {'test-address.eth': web3.eth.accounts[0]}): + if expect_success: + balance = web3.eth.getBalance(address) + assert is_integer(balance) + assert balance >= 0 + else: + with pytest.raises(NameNotFound): + web3.eth.getBalance(address) + def test_eth_getStorageAt( self, web3: "Web3", emitter_contract_address: ChecksumAddress ) -> None: storage = web3.eth.getStorageAt(emitter_contract_address, 0) assert isinstance(storage, HexBytes) + def test_eth_getStorageAt_ens_name( + self, web3: "Web3", emitter_contract_address: ChecksumAddress + ) -> None: + with ens_addresses(web3, {'emitter.eth': emitter_contract_address}): + storage = web3.eth.getStorageAt('emitter.eth', 0) + assert isinstance(storage, HexBytes) + def test_eth_getStorageAt_invalid_address(self, web3: "Web3") -> None: coinbase = web3.eth.coinbase with pytest.raises(InvalidAddress): @@ -154,6 +181,14 @@ def test_eth_getTransactionCount( assert is_integer(transaction_count) assert transaction_count >= 0 + def test_eth_getTransactionCount_ens_name( + self, web3: "Web3", unlocked_account_dual_type: ChecksumAddress + ) -> None: + with ens_addresses(web3, {'unlocked-acct-dual-type.eth': unlocked_account_dual_type}): + transaction_count = web3.eth.getTransactionCount('unlocked-acct-dual-type.eth') + assert is_integer(transaction_count) + assert transaction_count >= 0 + def test_eth_getTransactionCount_invalid_address(self, web3: "Web3") -> None: coinbase = web3.eth.coinbase with pytest.raises(InvalidAddress): @@ -208,6 +243,16 @@ def test_eth_getCode(self, web3: "Web3", math_contract_address: ChecksumAddress) assert isinstance(code, HexBytes) assert len(code) > 0 + def test_eth_getCode_ens_address( + self, web3: "Web3", math_contract_address: ChecksumAddress + ) -> None: + with ens_addresses( + web3, {'mathcontract.eth': math_contract_address} + ): + code = web3.eth.getCode('mathcontract.eth') + assert isinstance(code, HexBytes) + assert len(code) > 0 + def test_eth_getCode_invalid_address(self, web3: "Web3", math_contract: "Contract") -> None: with pytest.raises(InvalidAddress): web3.eth.getCode(ChecksumAddress(HexAddress(HexStr(math_contract.address.lower())))) @@ -251,6 +296,16 @@ def test_eth_sign(self, web3: "Web3", unlocked_account_dual_type: ChecksumAddres ) assert new_signature != signature + def test_eth_sign_ens_names( + self, web3: "Web3", unlocked_account_dual_type: ChecksumAddress + ) -> None: + with ens_addresses(web3, {'unlocked-acct.eth': unlocked_account_dual_type}): + signature = web3.eth.sign( + 'unlocked-acct.eth', text='Message tö sign. Longer than hash!' + ) + assert is_bytes(signature) + assert len(signature) == 32 + 32 + 1 + def test_eth_signTypedData( self, web3: "Web3", @@ -374,6 +429,27 @@ def test_eth_signTransaction(self, web3: "Web3", unlocked_account: ChecksumAddre assert result['tx']['gasPrice'] == txn_params['gasPrice'] assert result['tx']['nonce'] == txn_params['nonce'] + def test_eth_signTransaction_ens_names( + self, web3: "Web3", unlocked_account: ChecksumAddress + ) -> None: + with ens_addresses(web3, {'unlocked-account.eth': unlocked_account}): + txn_params: TxParams = { + 'from': 'unlocked-account.eth', + 'to': 'unlocked-account.eth', + 'value': Wei(1), + 'gas': Wei(21000), + 'gasPrice': web3.eth.gasPrice, + 'nonce': Nonce(0), + } + result = web3.eth.signTransaction(txn_params) + signatory_account = web3.eth.account.recover_transaction(result['raw']) + assert unlocked_account == signatory_account + assert result['tx']['to'] == unlocked_account + assert result['tx']['value'] == txn_params['value'] + assert result['tx']['gas'] == txn_params['gas'] + assert result['tx']['gasPrice'] == txn_params['gasPrice'] + assert result['tx']['nonce'] == txn_params['nonce'] + def test_eth_sendTransaction_addr_checksum_required( self, web3: "Web3", unlocked_account: ChecksumAddress ) -> None: diff --git a/web3/_utils/validation.py b/web3/_utils/validation.py index f16555745f..65fcb9f1c0 100644 --- a/web3/_utils/validation.py +++ b/web3/_utils/validation.py @@ -34,6 +34,9 @@ valmap, ) +from ens.utils import ( + is_valid_ens_name, +) from web3._utils.abi import ( abi_to_signature, filter_by_type, @@ -152,10 +155,19 @@ def validate_abi_value(abi_type: TypeStr, value: Any) -> None: ) +def is_not_address_string(value: Any) -> bool: + return (is_string(value) and not is_bytes(value) and not + is_checksum_address(value) and not is_hex_address(value)) + + def validate_address(value: Any) -> None: """ Helper function for validating an address """ + if is_not_address_string(value): + if not is_valid_ens_name(value): + raise InvalidAddress(f"ENS name: '{value}' is invalid.") + return if is_bytes(value): if not is_binary_address(value): raise InvalidAddress("Address must be 20 bytes when input type is bytes", value) diff --git a/web3/eth.py b/web3/eth.py index dd0f73bf04..d2abd108eb 100644 --- a/web3/eth.py +++ b/web3/eth.py @@ -52,9 +52,6 @@ LogFilter, TransactionFilter, ) -from web3._utils.normalizers import ( - abi_ens_resolver, -) from web3._utils.rpc_abi import ( RPC, ) @@ -203,50 +200,60 @@ def blockNumber(self) -> BlockNumber: def chainId(self) -> int: return self.web3.manager.request_blocking(RPC.eth_chainId, []) - def block_identifier_munger( - self, - *args: Any, - block_identifier: BlockIdentifier=None - ) -> List[Any]: - if block_identifier is None: - block_identifier = self.defaultBlock - return [*args, block_identifier] - - def address_resolver_munger( + def block_id_munger( self, account: Union[Address, ChecksumAddress, ENS], block_identifier: Optional[BlockIdentifier]=None ) -> Tuple[Union[Address, ChecksumAddress, ENS], BlockIdentifier]: - resolved_addr = abi_ens_resolver(self.web3, 'address', account)[1] if block_identifier is None: block_identifier = self.defaultBlock - return (resolved_addr, block_identifier) + return (account, block_identifier) getBalance: Method[Callable[..., Wei]] = Method( RPC.eth_getBalance, - mungers=[address_resolver_munger], + mungers=[block_id_munger], ) + def get_storage_at_munger( + self, + account: Union[Address, ChecksumAddress, ENS], + position: int, + block_identifier: Optional[BlockIdentifier]=None + ) -> Tuple[Union[Address, ChecksumAddress, ENS], int, BlockIdentifier]: + if block_identifier is None: + block_identifier = self.defaultBlock + return (account, position, block_identifier) + getStorageAt: Method[ Callable[..., HexBytes] ] = Method( RPC.eth_getStorageAt, - mungers=[block_identifier_munger], + mungers=[get_storage_at_munger], ) + def get_proof_munger( + self, + account: Union[Address, ChecksumAddress, ENS], + positions: Sequence[int], + block_identifier: Optional[BlockIdentifier]=None + ) -> Tuple[Union[Address, ChecksumAddress, ENS], Sequence[int], Optional[BlockIdentifier]]: + if block_identifier is None: + block_identifier = self.defaultBlock + return (account, positions, block_identifier) + getProof: Method[ Callable[ - [Tuple[ChecksumAddress, Sequence[int], Optional[BlockIdentifier]]], + [Tuple[Union[Address, ChecksumAddress, ENS], Sequence[int], Optional[BlockIdentifier]]], MerkleProof ] ] = Method( RPC.eth_getProof, - mungers=[block_identifier_munger], + mungers=[get_proof_munger], ) getCode: Method[Callable[..., HexBytes]] = Method( RPC.eth_getCode, - mungers=[address_resolver_munger] + mungers=[block_id_munger] ) def get_block_munger( @@ -349,7 +356,7 @@ def waitForTransactionReceipt( getTransactionCount: Method[Callable[..., Nonce]] = Method( RPC.eth_getTransactionCount, - mungers=[block_identifier_munger], + mungers=[block_id_munger], ) def replaceTransaction(self, transaction_hash: _Hash32, new_transaction: TxParams) -> HexBytes: