diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index b25d77a..6413b26 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -31,7 +31,7 @@ jobs: run: curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 - name: Install dependencies run: | - pdm sync -G lint,compiler,local-provider,http-provider-server + pdm sync -G lint,compiler,local-provider,http-provider-server,tests - name: Run mypy run: | - pdm run mypy pons + pdm run mypy pons tests diff --git a/docs/api.rst b/docs/api.rst index 306be39..cd16ff3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -51,6 +51,9 @@ Errors .. autoclass:: pons.ABIDecodingError :show-inheritance: +.. autoclass:: pons.InvalidResponse + :show-inheritance: + .. autoclass:: pons.Unreachable :show-inheritance: diff --git a/docs/changelog.rst b/docs/changelog.rst index 6cff41d..d2f8ef7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,11 +21,14 @@ Added - A base class for bound calls (``BaseBoundMethodCall``). (PR_84_) - A helper class for interacting with the Multicall contract (``Multicall``). (PR_84_) - Exporting ``HTTPError`` and ``BadResponseFormat``. (PR_82_) +- Exporting ``InvalidResponse``. (PR_86_) +- ``LocalProvider.root`` is now of type ``AccountSigner`` instead of ``Signer``. (PR_86_) .. _PR_81: https://github.com/fjarri-eth/pons/pull/81 .. _PR_82: https://github.com/fjarri-eth/pons/pull/82 .. _PR_84: https://github.com/fjarri-eth/pons/pull/84 +.. _PR_86: https://github.com/fjarri-eth/pons/pull/86 0.8.1 (2024-11-12) diff --git a/pons/__init__.py b/pons/__init__.py index c568d0c..acb6fa7 100644 --- a/pons/__init__.py +++ b/pons/__init__.py @@ -48,7 +48,14 @@ from ._http_provider_server import HTTPProviderServer from ._local_provider import LocalProvider, SnapshotID from ._multicall import BoundMultiMethodCall, BoundMultiMethodValueCall, Multicall -from ._provider import HTTPError, HTTPProvider, ProtocolError, Provider, Unreachable +from ._provider import ( + HTTPError, + HTTPProvider, + InvalidResponse, + ProtocolError, + Provider, + Unreachable, +) from ._signer import AccountSigner, Signer from ._utils import get_create2_address, get_create_address @@ -90,6 +97,7 @@ "HTTPError", "HTTPProvider", "HTTPProviderServer", + "InvalidResponse", "LocalProvider", "Method", "MethodCall", diff --git a/pons/_local_provider.py b/pons/_local_provider.py index 4eccd99..6baf67d 100644 --- a/pons/_local_provider.py +++ b/pons/_local_provider.py @@ -11,7 +11,7 @@ from ethereum_rpc import Amount from ._provider import RPC_JSON, Provider, ProviderSession -from ._signer import AccountSigner, Signer +from ._signer import AccountSigner class SnapshotID: @@ -24,7 +24,7 @@ def __init__(self, id_: int): class LocalProvider(Provider): """A provider maintaining its own chain state, useful for tests.""" - root: Signer + root: AccountSigner """The signer for the pre-created account.""" def __init__( diff --git a/pons/_provider.py b/pons/_provider.py index 10bbee4..9ac9f66 100644 --- a/pons/_provider.py +++ b/pons/_provider.py @@ -77,13 +77,10 @@ class ProviderSession(ABC): The base class for provider sessions. The methods of this class may raise the following exceptions: - - :py:class:`RPCError` signifies an error coming from the backend provider; - - :py:class:`Unreachable` if the provider is unreachable; - - :py:class:`InvalidResponse` if the response was received but could not be parsed; - - :py:class:`ProtocolError` if there was an unrecognized error on the protocol level - (e.g. an HTTP status code that is not 200 or 400). - - All other exceptions can be considered implementation bugs. + :py:class:`RPCError`, + :py:class:`Unreachable`, + :py:class:`InvalidResponse`, + a provider-specific derived class of :py:class:`ProtocolError`. """ @abstractmethod diff --git a/pyproject.toml b/pyproject.toml index c81047e..1338d57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,6 +137,8 @@ ignore = [ "ISC001", # Too many false positives, since Ruff cannot tell that the object is immutable. "RUF009", + # Way too restrictive. Sometimes it is useful to import types from pytest. + "PT013", ] # Allow autofix for all enabled rules (when `--fix`) is provided. diff --git a/tests/conftest.py b/tests/conftest.py index 9051354..2a286a5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,27 +1,29 @@ +from collections.abc import AsyncIterator + import pytest from alysis import EVMVersion from ethereum_rpc import Amount -from pons import AccountSigner, Client, LocalProvider +from pons import AccountSigner, Client, ClientSession, LocalProvider @pytest.fixture -def local_provider(): +def local_provider() -> LocalProvider: return LocalProvider(root_balance=Amount.ether(100), evm_version=EVMVersion.CANCUN) @pytest.fixture -async def session(local_provider): +async def session(local_provider: LocalProvider) -> AsyncIterator[ClientSession]: client = Client(provider=local_provider) async with client.session() as session: yield session @pytest.fixture -def root_signer(local_provider): +def root_signer(local_provider: LocalProvider) -> AccountSigner: return local_provider.root @pytest.fixture -def another_signer(): +def another_signer() -> AccountSigner: return AccountSigner.create() diff --git a/tests/test_abi_types.py b/tests/test_abi_types.py index cd083f9..40b1464 100644 --- a/tests/test_abi_types.py +++ b/tests/test_abi_types.py @@ -1,11 +1,15 @@ import os +from typing import Any import pytest from ethereum_rpc import Address, keccak from pons import abi from pons._abi_types import ( + ABI_JSON, ABIDecodingError, + ABIType, + Type, decode_args, dispatch_type, dispatch_types, @@ -14,7 +18,7 @@ ) -def test_uint(): +def test_uint() -> None: for val in [0, 255, 10]: assert abi.uint(8)._normalize(val) == val assert abi.uint(8)._denormalize(val) == val @@ -45,7 +49,7 @@ def test_uint(): abi.uint(128)._normalize(2**128) -def test_int(): +def test_int() -> None: for val in [0, 127, -128, 10]: assert abi.int(8)._normalize(val) == val assert abi.int(8)._denormalize(val) == val @@ -78,7 +82,7 @@ def test_int(): abi.int(128)._normalize(2**127) -def test_bytes(): +def test_bytes() -> None: assert abi.bytes(3)._normalize(b"foo") == b"foo" assert abi.bytes(3)._denormalize(b"foo") == b"foo" assert abi.bytes()._normalize(b"foobar") == b"foobar" @@ -102,7 +106,7 @@ def test_bytes(): abi.bytes(4)._normalize(b"foo") -def test_address(): +def test_address() -> None: addr_bytes = os.urandom(20) addr = Address(addr_bytes) @@ -122,7 +126,7 @@ def test_address(): abi.address._denormalize(addr_bytes) -def test_string(): +def test_string() -> None: assert abi.string._normalize("foo") == "foo" assert abi.string._denormalize("foo") == "foo" @@ -136,7 +140,7 @@ def test_string(): abi.string._normalize(b"foo") -def test_bool(): +def test_bool() -> None: assert abi.bool._normalize(True) is True assert abi.bool._denormalize(True) is True @@ -148,7 +152,7 @@ def test_bool(): abi.bool._normalize(1) -def test_array(): +def test_array() -> None: assert abi.uint(8)[2]._normalize([1, 2]) == [1, 2] assert abi.uint(8)[2]._denormalize([1, 2]) == [1, 2] assert abi.uint(8)[...]._normalize([1, 2, 3]) == [1, 2, 3] @@ -171,7 +175,7 @@ def test_array(): abi.uint(8)[2]._normalize([1, 2, 3]) -def test_struct(): +def test_struct() -> None: u8 = abi.uint(8) s1 = abi.struct(a=u8, b=abi.bool) @@ -198,7 +202,7 @@ def test_struct(): s1._normalize(dict(a=1, c=True)) -def test_type_from_abi_string(): +def test_type_from_abi_string() -> None: assert type_from_abi_string("uint32") == abi.uint(32) assert type_from_abi_string("int64") == abi.int(64) assert type_from_abi_string("bytes11") == abi.bytes(11) @@ -210,11 +214,11 @@ def test_type_from_abi_string(): type_from_abi_string("uintx") -def test_dispatch_type(): +def test_dispatch_type() -> None: assert dispatch_type(dict(type="uint8")) == abi.uint(8) assert dispatch_type(dict(type="uint8[2][]")) == abi.uint(8)[2][...] - struct_array = dict( + struct_array: ABI_JSON = dict( type="tuple[2]", components=[ dict(name="field1", type="bool"), @@ -229,7 +233,7 @@ def test_dispatch_type(): dispatch_type(dict(type="uint8(2)[3]")) -def test_dispatch_types(): +def test_dispatch_types() -> None: entries = [ dict(name="param2", type="uint8"), dict(name="param1", type="uint16[2]"), @@ -238,7 +242,9 @@ def test_dispatch_types(): assert dispatch_types(entries) == dict(param2=abi.uint(8), param1=abi.uint(16)[2]) # Check that the order is preserved, too - assert list(dispatch_types(entries).items()) == [ + typed_entries = dispatch_types(entries) + assert isinstance(typed_entries, dict) + assert list(typed_entries.items()) == [ ("param2", abi.uint(8)), ("param1", abi.uint(16)[2]), ] @@ -259,15 +265,15 @@ def test_dispatch_types(): dispatch_types([dict(name="foo", type="uint8"), dict(name="foo", type="uint16[2]")]) -def test_making_arrays(): +def test_making_arrays() -> None: assert abi.uint(8)[2].canonical_form == "uint8[2]" assert abi.uint(8)[...][3][...].canonical_form == "uint8[][3][]" with pytest.raises(TypeError, match="Invalid array size specifier type: float"): - abi.uint(8)[1.0] + abi.uint(8)[1.0] # type: ignore[index] -def test_normalization_roundtrip(): +def test_normalization_roundtrip() -> None: struct = abi.struct( field1=abi.uint(8), field2=abi.uint(16)[2], @@ -279,7 +285,7 @@ def test_normalization_roundtrip(): value = dict(field1=1, field2=[2, 3], field3=addr, field4=dict(inner2="abcd", inner1=True)) - expected_normalized = [1, [2, 3], addr.checksum, [True, "abcd"]] + expected_normalized: ABIType = [1, [2, 3], addr.checksum, [True, "abcd"]] # normalize() loses info on struct field names assert struct._normalize(value) == expected_normalized @@ -288,7 +294,9 @@ def test_normalization_roundtrip(): assert struct._denormalize(expected_normalized) == value -def check_topic_encode_decode(tp, val, encoded_val, *, can_be_decoded=True): +def check_topic_encode_decode( + tp: Type, val: Any, encoded_val: bytes, *, can_be_decoded: bool = True +) -> None: assert tp.encode_to_topic(val) == encoded_val if can_be_decoded: assert tp.decode_from_topic(encoded_val) == val @@ -296,7 +304,7 @@ def check_topic_encode_decode(tp, val, encoded_val, *, can_be_decoded=True): assert tp.decode_from_topic(encoded_val) is None -def test_encode_to_topic(): +def test_encode_to_topic() -> None: # Simple types check_topic_encode_decode( @@ -331,14 +339,14 @@ def test_encode_to_topic(): tp = abi.bytes()[2] small_bytes = os.urandom(5) big_bytes = os.urandom(33) - val = [small_bytes, big_bytes] + val: Any = [small_bytes, big_bytes] # Values in the array are padded to multiples of 32 bytes encoded_val = keccak(small_bytes + b"\x00" * 27 + big_bytes + b"\x00" * 31) check_topic_encode_decode(tp, val, encoded_val, can_be_decoded=False) # Structs - tp = abi.struct( + struct_tp = abi.struct( field1=abi.bytes()[2], field2=abi.bool, field3=abi.struct(inner1=abi.bytes(5), inner2=abi.string), @@ -355,10 +363,10 @@ def test_encode_to_topic(): + (small_bytes + b"\x00" * 27) + (string.encode() + b"\x00" * 28) ) - check_topic_encode_decode(tp, val, encoded_val, can_be_decoded=False) + check_topic_encode_decode(struct_tp, val, encoded_val, can_be_decoded=False) -def test_encode_decode_args(): +def test_encode_decode_args() -> None: args = ("some string", b"bytestring", 1234) types = [abi.string, abi.bytes(), abi.uint(256)] encoded = encode_args(*zip(types, args, strict=True)) @@ -368,7 +376,7 @@ def test_encode_decode_args(): assert encode_args() == b"" -def test_decoding_error(): +def test_decoding_error() -> None: types = [abi.uint(256), abi.uint(256)] encoded_bytes = b"\x00" * 31 + b"\x01" # Only one uint256 diff --git a/tests/test_client.py b/tests/test_client.py index e40f953..9cbeaa6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,21 +1,29 @@ import re -from contextlib import contextmanager from pathlib import Path +from typing import Any import pytest import trio from ethereum_rpc import ( Amount, BlockHash, + ErrorCode, RPCError, RPCErrorCode, TxHash, TxInfo, + TxReceipt, ) +from pytest import MonkeyPatch +from trio.testing import MockClock from pons import ( ABIDecodingError, + AccountSigner, + BoundEvent, Client, + ClientSession, + CompiledContract, ContractABI, ContractError, ContractLegacyError, @@ -30,49 +38,46 @@ from pons._abi_types import encode_args from pons._client import BadResponseFormat, ProviderError, TransactionFailed from pons._contract_abi import PANIC_ERROR +from pons._provider import RPC_JSON @pytest.fixture -def compiled_contracts(): +def compiled_contracts() -> dict[str, CompiledContract]: path = Path(__file__).resolve().parent / "TestClient.sol" return compile_contract_file(path) -@contextmanager -def monkeypatched(obj, attr, patch): - original_value = getattr(obj, attr) - setattr(obj, attr, patch) - yield obj - setattr(obj, attr, original_value) - - -async def test_net_version(local_provider, session): +async def test_net_version( + local_provider: LocalProvider, session: ClientSession, monkeypatch: MonkeyPatch +) -> None: net_version1 = await session.net_version() assert net_version1 == "1" # This is not going to get called - def mock_rpc(*_args): + def mock_rpc(*_args: Any) -> RPC_JSON: raise NotImplementedError # pragma: no cover # The result should have been cached the first time - with monkeypatched(local_provider, "rpc", mock_rpc): - net_version2 = await session.net_version() + monkeypatch.setattr(local_provider, "rpc", mock_rpc) + net_version2 = await session.net_version() assert net_version1 == net_version2 -async def test_net_version_type_check(local_provider, session): +async def test_net_version_type_check( + local_provider: LocalProvider, session: ClientSession, monkeypatch: MonkeyPatch +) -> None: # Provider returning a bad value - with monkeypatched(local_provider, "rpc", lambda *_args: 0): - with pytest.raises(BadResponseFormat, match="net_version: The value must be a string"): - await session.net_version() + monkeypatch.setattr(local_provider, "rpc", lambda *_args: 0) + with pytest.raises(BadResponseFormat, match="net_version: The value must be a string"): + await session.net_version() -async def test_chain_id(): +async def test_chain_id(monkeypatch: MonkeyPatch) -> None: local_provider = LocalProvider(root_balance=Amount.ether(100), chain_id=123) client = Client(local_provider) # This is not going to get called - def mock_rpc(_method, *_args): + def mock_rpc(_method: str, *_args: Any) -> RPC_JSON: raise NotImplementedError # pragma: no cover async with client.session() as session: @@ -80,23 +85,29 @@ def mock_rpc(_method, *_args): assert chain_id1 == 123 # The result should have been cached the first time - with monkeypatched(local_provider, "rpc", mock_rpc): - chain_id2 = await session.chain_id() + monkeypatch.setattr(local_provider, "rpc", mock_rpc) + chain_id2 = await session.chain_id() assert chain_id1 == chain_id2 -async def test_get_block(session, root_signer, another_signer): +async def test_get_block( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: to_transfer = Amount.ether(10) await session.transfer(root_signer, another_signer.address, to_transfer) block_info = await session.get_block(1, with_transactions=True) + assert block_info is not None assert all(isinstance(tx, TxInfo) for tx in block_info.transactions) + assert block_info.hash_ is not None block_info2 = await session.get_block(block_info.hash_, with_transactions=True) + assert block_info2 is not None assert block_info2 == block_info # no transactions block_info = await session.get_block(1) + assert block_info is not None assert all(isinstance(tx, TxHash) for tx in block_info.transactions) # non-existent block @@ -107,12 +118,12 @@ async def test_get_block(session, root_signer, another_signer): async def test_wait_for_transaction_receipt( - autojump_clock, # noqa: ARG001 - local_provider, - session, - root_signer, - another_signer, -): + autojump_clock: MockClock, # noqa: ARG001 + local_provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: to_transfer = Amount.ether(10) local_provider.disable_auto_mine_transactions() tx_hash = await session.broadcast_transfer(root_signer, another_signer.address, to_transfer) @@ -122,18 +133,18 @@ async def test_wait_for_transaction_receipt( start_time = trio.current_time() with pytest.raises(trio.TooSlowError): with trio.fail_after(timeout): - receipt = await session.wait_for_transaction_receipt(tx_hash) + _receipt = await session.wait_for_transaction_receipt(tx_hash) end_time = trio.current_time() assert end_time - start_time == timeout # Now let's enable mining while we wait for the receipt - receipt = None + receipt: None | TxReceipt = None - async def get_receipt(): + async def get_receipt() -> None: nonlocal receipt receipt = await session.wait_for_transaction_receipt(tx_hash) - async def delayed_enable_mining(): + async def delayed_enable_mining() -> None: await trio.sleep(timeout) local_provider.enable_auto_mine_transactions() @@ -141,10 +152,15 @@ async def delayed_enable_mining(): nursery.start_soon(get_receipt) nursery.start_soon(delayed_enable_mining) + assert receipt is not None assert receipt.succeeded -async def test_call_contract_error(session, compiled_contracts, root_signer): +async def test_call_contract_error( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, +) -> None: """Tests that `call()` correctly decodes a contract error.""" compiled_contract = compiled_contracts["TestErrors"] deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123)) @@ -155,7 +171,11 @@ async def test_call_contract_error(session, compiled_contracts, root_signer): assert exc.value.data == {"x": 4} -async def test_call_decoding_error(session, compiled_contracts, root_signer): +async def test_call_decoding_error( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, +) -> None: """ Tests that `call()` propagates an error on mismatch of the declared output signature and the bytestring received from the provider (as opposed to wrapping it in another exception). @@ -185,14 +205,20 @@ async def test_call_decoding_error(session, compiled_contracts, root_signer): await session.call(wrong_contract.method.getState(456)) -async def test_estimate_deploy(session, compiled_contracts, root_signer): +async def test_estimate_deploy( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, +) -> None: compiled_contract = compiled_contracts["BasicContract"] gas = await session.estimate_deploy(root_signer.address, compiled_contract.constructor(1)) assert isinstance(gas, int) assert gas > 0 -async def test_estimate_transfer(session, root_signer, another_signer): +async def test_estimate_transfer( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: gas = await session.estimate_transfer( root_signer.address, another_signer.address, Amount.ether(10) ) @@ -208,7 +234,11 @@ async def test_estimate_transfer(session, root_signer, another_signer): ) -async def test_estimate_transact(session, compiled_contracts, root_signer): +async def test_estimate_transact( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, +) -> None: compiled_contract = compiled_contracts["BasicContract"] deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(1)) gas = await session.estimate_transact( @@ -218,7 +248,9 @@ async def test_estimate_transact(session, compiled_contracts, root_signer): assert gas > 0 -async def test_transfer(session, root_signer, another_signer): +async def test_transfer( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: # Regular transfer root_balance = await session.get_balance(root_signer.address) to_transfer = Amount.ether(10) @@ -229,7 +261,9 @@ async def test_transfer(session, root_signer, another_signer): assert root_balance - root_balance_after > to_transfer -async def test_transfer_custom_gas(session, root_signer, another_signer): +async def test_transfer_custom_gas( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: root_balance = await session.get_balance(root_signer.address) to_transfer = Amount.ether(10) @@ -248,24 +282,37 @@ async def test_transfer_custom_gas(session, root_signer, another_signer): await session.transfer(root_signer, another_signer.address, to_transfer, gas=20000) -async def test_transfer_failed(local_provider, session, root_signer, another_signer): +async def test_transfer_failed( + local_provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, + monkeypatch: MonkeyPatch, +) -> None: # TODO: it would be nice to reproduce the actual situation where this could happen # (tranfer was accepted for mining, but failed in the process, # and the resulting receipt has a 0 status). orig_rpc = local_provider.rpc - def mock_rpc(method, *args): + def mock_rpc(method: str, *args: Any) -> RPC_JSON: result = orig_rpc(method, *args) if method == "eth_getTransactionReceipt": + assert isinstance(result, dict) result["status"] = "0x0" return result - with monkeypatched(local_provider, "rpc", mock_rpc): - with pytest.raises(TransactionFailed, match="Transfer failed"): - await session.transfer(root_signer, another_signer.address, Amount.ether(10)) + monkeypatch.setattr(local_provider, "rpc", mock_rpc) + with pytest.raises(TransactionFailed, match="Transfer failed"): + await session.transfer(root_signer, another_signer.address, Amount.ether(10)) -async def test_deploy(local_provider, session, compiled_contracts, root_signer): +async def test_deploy( + local_provider: LocalProvider, + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + monkeypatch: MonkeyPatch, +) -> None: basic_contract = compiled_contracts["BasicContract"] construction_error = compiled_contracts["TestErrors"] payable_constructor = compiled_contracts["PayableConstructor"] @@ -296,24 +343,28 @@ async def test_deploy(local_provider, session, compiled_contracts, root_signer): # Test the provider returning an empty `contractAddress` orig_rpc = local_provider.rpc - def mock_rpc(method, *args): + def mock_rpc(method: str, *args: Any) -> RPC_JSON: result = orig_rpc(method, *args) if method == "eth_getTransactionReceipt": + assert isinstance(result, dict) result["contractAddress"] = None return result - with monkeypatched(local_provider, "rpc", mock_rpc): - with pytest.raises( - BadResponseFormat, - match=( - "The deploy transaction succeeded, " - "but `contractAddress` is not present in the receipt" - ), - ): - await session.deploy(root_signer, basic_contract.constructor(0)) + monkeypatch.setattr(local_provider, "rpc", mock_rpc) + with pytest.raises( + BadResponseFormat, + match=( + "The deploy transaction succeeded, but `contractAddress` is not present in the receipt" + ), + ): + await session.deploy(root_signer, basic_contract.constructor(0)) -async def test_transact(session, compiled_contracts, root_signer): +async def test_transact( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, +) -> None: basic_contract = compiled_contracts["BasicContract"] # Normal transact @@ -346,8 +397,11 @@ async def test_transact(session, compiled_contracts, root_signer): async def test_transact_with_pending_state( - local_provider, session, compiled_contracts, root_signer -): + local_provider: LocalProvider, + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, +) -> None: # Test that a newly submitted transaction uses the pending state and not the finalized state basic_contract = compiled_contracts["BasicContract"] @@ -363,13 +417,13 @@ async def test_transact_with_pending_state( async def test_transact_and_return_events( - autojump_clock, # noqa: ARG001 - local_provider, - session, - compiled_contracts, - root_signer, - another_signer, -): + autojump_clock: MockClock, # noqa: ARG001 + local_provider: LocalProvider, + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: await session.transfer(root_signer, another_signer.address, Amount.ether(1)) basic_contract = compiled_contracts["BasicContract"] @@ -379,7 +433,7 @@ async def test_transact_and_return_events( event1 = deployed_contract.event.Event1 event2 = deployed_contract.event.Event2 - def results_for(x): + def results_for(x: int) -> dict[BoundEvent, list[dict[str, Any]]]: return { event1: [{"value": x}, {"value": x + 1}], event2: [{"value": x + 2}, {"value": x + 3}], @@ -402,13 +456,14 @@ def results_for(x): results = {} - async def transact(signer, x): + async def transact(signer: AccountSigner, x: int) -> None: + nonlocal results result = await session.transact( signer, deployed_contract.method.emitMultipleEvents(x), return_events=[event1, event2] ) results[x] = result - async def delayed_enable_mining(): + async def delayed_enable_mining() -> None: await trio.sleep(5) local_provider.enable_auto_mine_transactions() @@ -424,14 +479,14 @@ async def delayed_enable_mining(): async def test_block_filter_high_level( - autojump_clock, # noqa: ARG001 - session, - root_signer, - another_signer, -): + autojump_clock: MockClock, # noqa: ARG001 + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: block_hashes = [] - async def observer(): + async def observer() -> None: # This loop always exits via break async for block_hash in session.iter_blocks(poll_interval=1): # pragma: no branch block_hashes.append(block_hash) @@ -452,18 +507,21 @@ async def observer(): for i, block_hash in enumerate(block_hashes): block_info = await session.get_block(block_hash, with_transactions=True) + assert block_info is not None + assert isinstance(block_info.transactions[0], TxInfo) assert block_info.transactions[0].value == Amount.ether(i + 2) async def test_pending_transaction_filter_high_level( - autojump_clock, # noqa: ARG001 - session, - root_signer, - another_signer, -): + autojump_clock: MockClock, # noqa: ARG001 + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: tx_hashes = [] - async def observer(): + async def observer() -> None: + nonlocal tx_hashes # This loop always exits via break async for tx_hash in session.iter_pending_transactions( # pragma: no branch poll_interval=1 @@ -486,16 +544,17 @@ async def observer(): for i, tx_hash in enumerate(tx_hashes): tx_info = await session.get_transaction(tx_hash) + assert tx_info is not None assert tx_info.value == Amount.ether(i + 2) async def test_event_filter_high_level( - autojump_clock, # noqa: ARG001 - session, - compiled_contracts, - root_signer, - another_signer, -): + autojump_clock: MockClock, # noqa: ARG001 + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: basic_contract = compiled_contracts["BasicContract"] await session.transfer(root_signer, another_signer.address, Amount.ether(1)) contract1 = await session.deploy(root_signer, basic_contract.constructor(123)) @@ -503,7 +562,8 @@ async def test_event_filter_high_level( events = [] - async def observer(): + async def observer() -> None: + nonlocal events event_filter = contract2.event.Deposit2(id=b"1111") # This loop always exits via break async for event in session.iter_events(event_filter, poll_interval=1): # pragma: no branch @@ -537,36 +597,46 @@ async def observer(): assert events[2] == {"from_": another_signer.address, "id": b"1111", "value": 3, "value2": 4} -async def test_contract_exceptions_high_level(session, root_signer, compiled_contracts): +async def test_contract_exceptions_high_level( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_contract = compiled_contracts["TestErrors"] contract = await session.deploy(root_signer, compiled_contract.constructor(999)) - with pytest.raises(ContractPanic) as exc: + with pytest.raises(ContractPanic) as panic_exc: await session.estimate_transact(root_signer.address, contract.method.transactPanic(1)) - assert exc.value.reason == ContractPanic.Reason.OVERFLOW + assert panic_exc.value.reason == ContractPanic.Reason.OVERFLOW - with pytest.raises(ContractLegacyError) as exc: + with pytest.raises(ContractLegacyError) as legacy_exc: await session.estimate_transact(root_signer.address, contract.method.transactError(0)) - assert exc.value.message == "" + assert legacy_exc.value.message == "" - with pytest.raises(ContractLegacyError) as exc: + with pytest.raises(ContractLegacyError) as legacy_exc: await session.estimate_transact(root_signer.address, contract.method.transactError(1)) - assert exc.value.message == "require(string)" + assert legacy_exc.value.message == "require(string)" - with pytest.raises(ContractError) as exc: + with pytest.raises(ContractError) as error_exc: await session.estimate_transact(root_signer.address, contract.method.transactError(4)) - assert exc.value.error == contract.error.CustomError - assert exc.value.data == {"x": 4} + assert error_exc.value.error == contract.error.CustomError + assert error_exc.value.data == {"x": 4} # Check that the same works for deployment - with pytest.raises(ContractError) as exc: + with pytest.raises(ContractError) as error_exc: await session.estimate_deploy(root_signer.address, compiled_contract.constructor(4)) - assert exc.value.error == contract.error.CustomError - assert exc.value.data == {"x": 4} + assert error_exc.value.error == contract.error.CustomError + assert error_exc.value.data == {"x": 4} -async def test_unknown_error_reasons(local_provider, session, compiled_contracts, root_signer): +async def test_unknown_error_reasons( + local_provider: LocalProvider, + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + monkeypatch: MonkeyPatch, +) -> None: compiled_contract = compiled_contracts["TestErrors"] contract = await session.deploy(root_signer, compiled_contract.constructor(999)) @@ -574,27 +644,29 @@ async def test_unknown_error_reasons(local_provider, session, compiled_contracts # Provider returns an unknown panic code - def mock_rpc(method, *args): + def mock_rpc_unknown_panic(method: str, *args: Any) -> RPC_JSON: if method == "eth_estimateGas": # Invalid selector data = PANIC_ERROR.selector + encode_args((abi.uint(256), 888)) - raise RPCError(RPCErrorCode.EXECUTION_ERROR, "execution reverted", data) + raise RPCError.with_code(RPCErrorCode.EXECUTION_ERROR, "execution reverted", data) return orig_rpc(method, *args) - with monkeypatched(local_provider, "rpc", mock_rpc): + with monkeypatch.context() as mp: + mp.setattr(local_provider, "rpc", mock_rpc_unknown_panic) with pytest.raises(ContractPanic, match=r"ContractPanicReason.UNKNOWN"): await session.estimate_transact(root_signer.address, contract.method.transactPanic(999)) # Provider returns an unknown error (a selector not present in the ABI) - def mock_rpc(method, *args): + def mock_rpc_unknown_error(method: str, *args: Any) -> RPC_JSON: if method == "eth_estimateGas": # Invalid selector data = b"1234" + encode_args((abi.uint(256), 1)) - raise RPCError(RPCErrorCode.EXECUTION_ERROR, "execution reverted", data) + raise RPCError.with_code(RPCErrorCode.EXECUTION_ERROR, "execution reverted", data) return orig_rpc(method, *args) - with monkeypatched(local_provider, "rpc", mock_rpc): + with monkeypatch.context() as mp: + mp.setattr(local_provider, "rpc", mock_rpc_unknown_error) with pytest.raises( ProviderError, match=r"Provider error \(RPCErrorCode\.EXECUTION_ERROR\): execution reverted", @@ -603,13 +675,14 @@ def mock_rpc(method, *args): # Provider returns an error with an unknown RPC code - def mock_rpc(method, *args): + def mock_rpc_unknown_code(method: str, *args: Any) -> RPC_JSON: if method == "eth_estimateGas": # Invalid selector data = PANIC_ERROR.selector + encode_args((abi.uint(256), 0)) - raise RPCError(12345, "execution reverted", data) + raise RPCError(ErrorCode(12345), "execution reverted", data) return orig_rpc(method, *args) - with monkeypatched(local_provider, "rpc", mock_rpc): + with monkeypatch.context() as mp: + mp.setattr(local_provider, "rpc", mock_rpc_unknown_code) with pytest.raises(ProviderError, match=r"Provider error \(12345\): execution reverted"): await session.estimate_transact(root_signer.address, contract.method.transactPanic(999)) diff --git a/tests/test_client_rpc.py b/tests/test_client_rpc.py index 1a671c3..392063a 100644 --- a/tests/test_client_rpc.py +++ b/tests/test_client_rpc.py @@ -1,6 +1,7 @@ import os -from contextlib import contextmanager +from collections.abc import Awaitable, Iterable from pathlib import Path +from typing import Any import pytest from ethereum_rpc import ( @@ -8,6 +9,9 @@ Amount, BlockHash, BlockLabel, + ErrorCode, + LogEntry, + LogTopic, RPCError, RPCErrorCode, TxHash, @@ -15,26 +19,27 @@ keccak, ) -from pons import Either, abi, compile_contract_file +from pons import ( + AccountSigner, + ClientSession, + CompiledContract, + Either, + LocalProvider, + abi, + compile_contract_file, +) from pons._abi_types import encode_args from pons._client_rpc import BadResponseFormat, ProviderError +from pons._provider import RPC_JSON @pytest.fixture -def compiled_contracts(): +def compiled_contracts() -> dict[str, CompiledContract]: path = Path(__file__).resolve().parent / "TestClient.sol" return compile_contract_file(path) -@contextmanager -def monkeypatched(obj, attr, patch): - original_value = getattr(obj, attr) - setattr(obj, attr, patch) - yield obj - setattr(obj, attr, original_value) - - -def normalize_topics(topics): +def normalize_topics(topics: Iterable[LogTopic]) -> tuple[tuple[LogTopic], ...]: """ Reduces visual noise in assertions by bringing the log topics in a log entry (a tuple of single elements) to the format used in EventFilter @@ -43,7 +48,9 @@ def normalize_topics(topics): return tuple((elem,) for elem in topics) -async def test_eth_get_balance(session, root_signer, another_signer): +async def test_eth_get_balance( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: to_transfer = Amount.ether(10) await session.transfer(root_signer, another_signer.address, to_transfer) acc1_balance = await session.rpc.eth_get_balance(another_signer.address) @@ -55,7 +62,12 @@ async def test_eth_get_balance(session, root_signer, another_signer): assert balance == Amount.ether(0) -async def test_eth_get_transaction_receipt(local_provider, session, root_signer, another_signer): +async def test_eth_get_transaction_receipt( + local_provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: local_provider.disable_auto_mine_transactions() tx_hash = await session.broadcast_transfer( root_signer, another_signer.address, Amount.ether(10) @@ -65,6 +77,7 @@ async def test_eth_get_transaction_receipt(local_provider, session, root_signer, local_provider.enable_auto_mine_transactions() receipt = await session.rpc.eth_get_transaction_receipt(tx_hash) + assert receipt is not None assert receipt.succeeded # A non-existent transaction @@ -72,7 +85,12 @@ async def test_eth_get_transaction_receipt(local_provider, session, root_signer, assert receipt is None -async def test_eth_get_transaction_count(local_provider, session, root_signer, another_signer): +async def test_eth_get_transaction_count( + local_provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: assert await session.rpc.eth_get_transaction_count(root_signer.address) == 0 await session.transfer(root_signer, another_signer.address, Amount.ether(10)) assert await session.rpc.eth_get_transaction_count(root_signer.address) == 1 @@ -83,33 +101,42 @@ async def test_eth_get_transaction_count(local_provider, session, root_signer, a assert await session.rpc.eth_get_transaction_count(root_signer.address, BlockLabel.PENDING) == 2 -async def test_eth_gas_price(session): +async def test_eth_gas_price(session: ClientSession) -> None: gas_price = await session.rpc.eth_gas_price() assert isinstance(gas_price, Amount) -async def test_eth_block_number(session, root_signer, another_signer): +async def test_eth_block_number( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: await session.transfer(root_signer, another_signer.address, Amount.ether(1)) await session.transfer(root_signer, another_signer.address, Amount.ether(2)) await session.transfer(root_signer, another_signer.address, Amount.ether(3)) block_num = await session.rpc.eth_block_number() block_info = await session.rpc.eth_get_block_by_number(block_num - 1, with_transactions=True) + assert block_info is not None + assert isinstance(block_info.transactions[0], TxInfo) assert block_info.transactions[0].value == Amount.ether(2) -async def test_eth_get_block(session, root_signer, another_signer): +async def test_eth_get_block( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: to_transfer = Amount.ether(10) await session.transfer(root_signer, another_signer.address, to_transfer) block_info = await session.rpc.eth_get_block_by_number(1, with_transactions=True) + assert block_info is not None assert all(isinstance(tx, TxInfo) for tx in block_info.transactions) + assert block_info.hash_ is not None block_info2 = await session.rpc.eth_get_block_by_hash(block_info.hash_, with_transactions=True) assert block_info2 == block_info # no transactions block_info = await session.rpc.eth_get_block_by_number(1) + assert block_info is not None assert all(isinstance(tx, TxHash) for tx in block_info.transactions) # non-existent block @@ -121,7 +148,12 @@ async def test_eth_get_block(session, root_signer, another_signer): assert block_info is None -async def test_eth_get_block_pending(local_provider, session, root_signer, another_signer): +async def test_eth_get_block_pending( + local_provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: await session.transfer(root_signer, another_signer.address, Amount.ether(1)) local_provider.disable_auto_mine_transactions() @@ -132,27 +164,36 @@ async def test_eth_get_block_pending(local_provider, session, root_signer, anoth block_info = await session.rpc.eth_get_block_by_number( BlockLabel.PENDING, with_transactions=True ) + assert block_info is not None assert block_info.number == 2 assert block_info.hash_ is None assert block_info.nonce is None assert block_info.miner is None assert block_info.total_difficulty is None assert len(block_info.transactions) == 1 + assert isinstance(block_info.transactions[0], TxInfo) assert block_info.transactions[0].hash_ == tx_hash assert block_info.transactions[0].value == Amount.ether(10) block_info = await session.rpc.eth_get_block_by_number( BlockLabel.PENDING, with_transactions=False ) + assert block_info is not None assert len(block_info.transactions) == 1 assert block_info.transactions[0] == tx_hash -async def test_eth_get_transaction_by_hash(local_provider, session, root_signer, another_signer): +async def test_eth_get_transaction_by_hash( + local_provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: to_transfer = Amount.ether(1) tx_hash = await session.broadcast_transfer(root_signer, another_signer.address, to_transfer) tx_info = await session.rpc.eth_get_transaction_by_hash(tx_hash) + assert tx_info is not None assert tx_info.value == to_transfer non_existent = TxHash(b"abcd" * 8) @@ -163,13 +204,18 @@ async def test_eth_get_transaction_by_hash(local_provider, session, root_signer, tx_hash = await session.broadcast_transfer(root_signer, another_signer.address, to_transfer) tx_info = await session.rpc.eth_get_transaction_by_hash(tx_hash) + assert tx_info is not None assert tx_info.block_number == 2 assert tx_info.block_hash is None assert tx_info.transaction_index is None assert tx_info.value == to_transfer -async def test_eth_get_code(session, root_signer, compiled_contracts): +async def test_eth_get_code( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_contract = compiled_contracts["EmptyContract"] deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123)) bytecode = await session.rpc.eth_get_code(deployed_contract.address, block=BlockLabel.LATEST) @@ -180,7 +226,11 @@ async def test_eth_get_code(session, root_signer, compiled_contracts): assert bytecode in compiled_contract.bytecode -async def test_eth_get_storage_at(session, root_signer, compiled_contracts): +async def test_eth_get_storage_at( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: x = 0xAB y_key = Address(os.urandom(20)) y_val = 0xCD @@ -214,7 +264,12 @@ async def test_eth_get_storage_at(session, root_signer, compiled_contracts): assert storage == b"\x00" * 31 + y_val.to_bytes(1, byteorder="big") -async def test_eth_call(session, compiled_contracts, root_signer, another_signer): +async def test_eth_call( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: compiled_contract = compiled_contracts["BasicContract"] deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123)) result = await session.call(deployed_contract.method.getState(456)) @@ -234,7 +289,12 @@ async def test_eth_call(session, compiled_contracts, root_signer, another_signer assert result == (another_signer.address,) -async def test_eth_call_pending(local_provider, session, compiled_contracts, root_signer): +async def test_eth_call_pending( + local_provider: LocalProvider, + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, +) -> None: compiled_contract = compiled_contracts["BasicContract"] deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123)) @@ -250,10 +310,12 @@ async def test_eth_call_pending(local_provider, session, compiled_contracts, roo assert result == (456,) -async def test_eth_get_filter_changes_bad_response(local_provider, session, monkeypatch): +async def test_eth_get_filter_changes_bad_response( + local_provider: LocalProvider, session: ClientSession, monkeypatch: pytest.MonkeyPatch +) -> None: block_filter = await session.rpc.eth_new_block_filter() - def mock_rpc(_method, *_args): + def mock_rpc(_method: str, *_args: Any) -> RPC_JSON: return {"foo": 1} monkeypatch.setattr(local_provider, "rpc", mock_rpc) @@ -265,7 +327,9 @@ def mock_rpc(_method, *_args): await session.rpc.eth_get_filter_changes(block_filter) -async def test_block_filter(session, root_signer, another_signer): +async def test_block_filter( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: to_transfer = Amount.ether(1) await session.transfer(root_signer, another_signer.address, to_transfer) @@ -276,7 +340,9 @@ async def test_block_filter(session, root_signer, another_signer): await session.transfer(root_signer, another_signer.address, to_transfer) last_block = await session.rpc.eth_get_block_by_number(BlockLabel.LATEST) + assert last_block is not None prev_block = await session.rpc.eth_get_block_by_number(last_block.number - 1) + assert prev_block is not None block_hashes = await session.rpc.eth_get_filter_changes(block_filter) assert block_hashes == (prev_block.hash_, last_block.hash_) @@ -285,13 +351,19 @@ async def test_block_filter(session, root_signer, another_signer): block_hashes = await session.rpc.eth_get_filter_changes(block_filter) last_block = await session.rpc.eth_get_block_by_number(BlockLabel.LATEST) + assert last_block is not None assert block_hashes == (last_block.hash_,) block_hashes = await session.rpc.eth_get_filter_changes(block_filter) assert len(block_hashes) == 0 -async def test_pending_transaction_filter(local_provider, session, root_signer, another_signer): +async def test_pending_transaction_filter( + local_provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: transaction_filter = await session.rpc.eth_new_pending_transaction_filter() to_transfer = Amount.ether(1) @@ -303,8 +375,13 @@ async def test_pending_transaction_filter(local_provider, session, root_signer, async def test_eth_get_logs( - monkeypatch, local_provider, session, compiled_contracts, root_signer, another_signer -): + monkeypatch: pytest.MonkeyPatch, + local_provider: LocalProvider, + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: basic_contract = compiled_contracts["BasicContract"] await session.transfer(root_signer, another_signer.address, Amount.ether(1)) contract1 = await session.deploy(root_signer, basic_contract.constructor(123)) @@ -337,7 +414,7 @@ async def test_eth_get_logs( # Test an invalid response - def mock_rpc(_method, *_args): + def mock_rpc(_method: str, *_args: Any) -> RPC_JSON: return {"foo": 1} monkeypatch.setattr(local_provider, "rpc", mock_rpc) @@ -349,7 +426,12 @@ def mock_rpc(_method, *_args): await session.rpc.eth_get_logs(source=contract2.address) -async def test_eth_get_filter_logs(session, compiled_contracts, root_signer, another_signer): +async def test_eth_get_filter_logs( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: basic_contract = compiled_contracts["BasicContract"] await session.transfer(root_signer, another_signer.address, Amount.ether(1)) contract1 = await session.deploy(root_signer, basic_contract.constructor(123)) @@ -361,6 +443,8 @@ async def test_eth_get_filter_logs(session, compiled_contracts, root_signer, ano entries = await session.rpc.eth_get_filter_logs(log_filter) assert len(entries) == 2 + assert isinstance(entries[0], LogEntry) + assert isinstance(entries[1], LogEntry) assert entries[0].address == contract1.address assert entries[1].address == contract2.address @@ -374,7 +458,12 @@ async def test_eth_get_filter_logs(session, compiled_contracts, root_signer, ano ) -async def test_log_filter_all(session, compiled_contracts, root_signer, another_signer): +async def test_log_filter_all( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: basic_contract = compiled_contracts["BasicContract"] await session.transfer(root_signer, another_signer.address, Amount.ether(1)) contract1 = await session.deploy(root_signer, basic_contract.constructor(123)) @@ -386,6 +475,8 @@ async def test_log_filter_all(session, compiled_contracts, root_signer, another_ entries = await session.rpc.eth_get_filter_changes(log_filter) assert len(entries) == 2 + assert isinstance(entries[0], LogEntry) + assert isinstance(entries[1], LogEntry) assert entries[0].address == contract1.address assert entries[1].address == contract2.address @@ -399,7 +490,12 @@ async def test_log_filter_all(session, compiled_contracts, root_signer, another_ ) -async def test_log_filter_by_address(session, compiled_contracts, root_signer, another_signer): +async def test_log_filter_by_address( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: basic_contract = compiled_contracts["BasicContract"] await session.transfer(root_signer, another_signer.address, Amount.ether(1)) contract1 = await session.deploy(root_signer, basic_contract.constructor(123)) @@ -413,6 +509,7 @@ async def test_log_filter_by_address(session, compiled_contracts, root_signer, a entries = await session.rpc.eth_get_filter_changes(log_filter) assert len(entries) == 1 + assert isinstance(entries[0], LogEntry) assert entries[0].address == contract2.address assert ( normalize_topics(entries[0].topics) @@ -430,6 +527,8 @@ async def test_log_filter_by_address(session, compiled_contracts, root_signer, a entries = await session.rpc.eth_get_filter_changes(log_filter) assert len(entries) == 2 + assert isinstance(entries[0], LogEntry) + assert isinstance(entries[1], LogEntry) assert entries[0].address == contract1.address assert ( normalize_topics(entries[0].topics) @@ -442,7 +541,12 @@ async def test_log_filter_by_address(session, compiled_contracts, root_signer, a ) -async def test_log_filter_by_topic(session, compiled_contracts, root_signer, another_signer): +async def test_log_filter_by_topic( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: basic_contract = compiled_contracts["BasicContract"] await session.transfer(root_signer, another_signer.address, Amount.ether(1)) contract1 = await session.deploy(root_signer, basic_contract.constructor(123)) @@ -464,6 +568,8 @@ async def test_log_filter_by_topic(session, compiled_contracts, root_signer, ano entries = await session.rpc.eth_get_filter_changes(log_filter) assert len(entries) == 2 + assert isinstance(entries[0], LogEntry) + assert isinstance(entries[1], LogEntry) assert entries[0].address == contract1.address assert ( normalize_topics(entries[0].topics) @@ -486,6 +592,8 @@ async def test_log_filter_by_topic(session, compiled_contracts, root_signer, ano entries = await session.rpc.eth_get_filter_changes(log_filter) assert len(entries) == 2 + assert isinstance(entries[0], LogEntry) + assert isinstance(entries[1], LogEntry) assert entries[0].address == contract1.address assert ( normalize_topics(entries[0].topics) @@ -498,7 +606,12 @@ async def test_log_filter_by_topic(session, compiled_contracts, root_signer, ano ) -async def test_log_filter_by_block_num(session, compiled_contracts, root_signer, another_signer): +async def test_log_filter_by_block_num( + session: ClientSession, + compiled_contracts: dict[str, CompiledContract], + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: basic_contract = compiled_contracts["BasicContract"] await session.transfer(root_signer, another_signer.address, Amount.ether(1)) contract1 = await session.deploy(root_signer, basic_contract.constructor(123)) @@ -515,19 +628,22 @@ async def test_log_filter_by_block_num(session, compiled_contracts, root_signer, entries = await session.rpc.eth_get_filter_changes(log_filter) # The range in the filter is inclusive - assert [entry.block_number for entry in entries] == list(range(block_num + 1, block_num + 4)) - assert [normalize_topics(entry.topics) for entry in entries] == [ - contract1.abi.event.Deposit(root_signer.address, b"2222").topics, - contract1.abi.event.Deposit(root_signer.address, b"3333").topics, - contract1.abi.event.Deposit(root_signer.address, b"4444").topics, - ] - - -async def test_unknown_rpc_status_code(local_provider, session, monkeypatch): - def mock_rpc(_method, *_args): + for i, entry in enumerate(entries): + assert isinstance(entry, LogEntry) + assert entry.block_number == block_num + i + 1 + assert ( + normalize_topics(entry.topics) + == contract1.abi.event.Deposit(root_signer.address, (str(i + 2) * 4).encode()).topics + ) + + +async def test_unknown_rpc_status_code( + local_provider: LocalProvider, session: ClientSession, monkeypatch: pytest.MonkeyPatch +) -> None: + def mock_rpc(_method: str, *_args: Any) -> RPC_JSON: # This is a known exception type, and it will be transferred through the network # keeping the status code. - raise RPCError(666, "this method is possessed") + raise RPCError(ErrorCode(666), "this method is possessed") monkeypatch.setattr(local_provider, "rpc", mock_rpc) @@ -535,7 +651,12 @@ def mock_rpc(_method, *_args): await session.net_version() -async def check_rpc_error(awaitable, expected_code, expected_message, expected_data): +async def check_rpc_error( + awaitable: Awaitable[Any], + expected_code: RPCErrorCode | None, + expected_message: str, + expected_data: bytes | None, +) -> None: with pytest.raises(ProviderError) as exc: await awaitable @@ -544,7 +665,11 @@ async def check_rpc_error(awaitable, expected_code, expected_message, expected_d assert exc.value.data == expected_data -async def test_contract_exceptions(session, root_signer, compiled_contracts): +async def test_contract_exceptions( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_contract = compiled_contracts["TestErrors"] contract = await session.deploy(root_signer, compiled_contract.constructor(999)) @@ -552,47 +677,51 @@ async def test_contract_exceptions(session, root_signer, compiled_contracts): custom_error_selector = keccak(b"CustomError(uint256)")[:4] # `require(condition)` - kwargs = dict( + await check_rpc_error( + session.rpc.eth_call(contract.method.viewError(0)), expected_code=RPCErrorCode.SERVER_ERROR, expected_message="execution reverted", expected_data=None, ) - await check_rpc_error(session.rpc.eth_call(contract.method.viewError(0)), **kwargs) # `require(condition, message)` - kwargs = dict( + await check_rpc_error( + session.rpc.eth_call(contract.method.viewError(1)), expected_code=RPCErrorCode.EXECUTION_ERROR, expected_message="execution reverted", expected_data=error_selector + encode_args((abi.string, "require(string)")), ) - await check_rpc_error(session.rpc.eth_call(contract.method.viewError(1)), **kwargs) # `revert()` - kwargs = dict( + await check_rpc_error( + session.rpc.eth_call(contract.method.viewError(2)), expected_code=RPCErrorCode.SERVER_ERROR, expected_message="execution reverted", expected_data=None, ) - await check_rpc_error(session.rpc.eth_call(contract.method.viewError(2)), **kwargs) # `revert(message)` - kwargs = dict( + await check_rpc_error( + session.rpc.eth_call(contract.method.viewError(3)), expected_code=RPCErrorCode.EXECUTION_ERROR, expected_message="execution reverted", expected_data=error_selector + encode_args((abi.string, "revert(string)")), ) - await check_rpc_error(session.rpc.eth_call(contract.method.viewError(3)), **kwargs) # `revert CustomError(...)` - kwargs = dict( + await check_rpc_error( + session.rpc.eth_call(contract.method.viewError(4)), expected_code=RPCErrorCode.EXECUTION_ERROR, expected_message="execution reverted", expected_data=custom_error_selector + encode_args((abi.uint(256), 4)), ) - await check_rpc_error(session.rpc.eth_call(contract.method.viewError(4)), **kwargs) -async def test_contract_panics(session, root_signer, compiled_contracts): +async def test_contract_panics( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_contract = compiled_contracts["TestErrors"] contract = await session.deploy(root_signer, compiled_contract.constructor(999)) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index d1cd317..49d1c61 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -3,13 +3,13 @@ from pons import EVMVersion, compile_contract_file -def test_multiple_contracts(): +def test_multiple_contracts() -> None: path = Path(__file__).resolve().parent / "TestCompiler.sol" contracts = compile_contract_file(path, evm_version=EVMVersion.SHANGHAI, optimize=True) assert sorted(contracts) == ["Contract1", "Contract2"] -def test_import_remappings(): +def test_import_remappings() -> None: root_path = path = Path(__file__).resolve().parent path = root_path / "TestCompilerWithImport.sol" contracts = compile_contract_file( diff --git a/tests/test_contract.py b/tests/test_contract.py index 4ee7a7d..ad29762 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -1,10 +1,10 @@ from pathlib import Path -from typing import NamedTuple import pytest -from ethereum_rpc import Address, LogTopic, keccak +from ethereum_rpc import Address, BlockHash, LogEntry, LogTopic, TxHash, keccak from pons import ( + CompiledContract, Constructor, Event, Fallback, @@ -18,12 +18,12 @@ @pytest.fixture -def compiled_contracts(): +def compiled_contracts() -> dict[str, CompiledContract]: path = Path(__file__).resolve().parent / "TestContract.sol" return compile_contract_file(path) -def test_abi_declaration(compiled_contracts): +def test_abi_declaration(compiled_contracts: dict[str, CompiledContract]) -> None: """Checks that the compiler output is parsed correctly.""" compiled_contract = compiled_contracts["JsonAbiTest"] @@ -66,7 +66,7 @@ def test_abi_declaration(compiled_contracts): assert cabi.event.Foo.indexed == {"x", "y"} -def test_api_binding(compiled_contracts): +def test_api_binding(compiled_contracts: dict[str, CompiledContract]) -> None: """Checks that the methods are bound correctly on deploy.""" compiled_contract = compiled_contracts["JsonAbiTest"] @@ -89,12 +89,14 @@ def test_api_binding(compiled_contracts): read_call = deployed_contract.method.getState(3) assert read_call.contract_address == address + assert isinstance(compiled_contract.abi.method.getState, Method) assert read_call.data_bytes == ( compiled_contract.abi.method.getState.selector + b"\x00" * 31 + b"\x03" ) assert read_call.decode_output(b"\x00" * 31 + b"\x04") == (4,) write_call = deployed_contract.method.setState(5) + assert isinstance(compiled_contract.abi.method.setState, Method) assert write_call.contract_address == address assert write_call.payable assert write_call.data_bytes == ( @@ -108,22 +110,34 @@ def test_api_binding(compiled_contracts): (LogTopic(keccak(b"1234")),), ) - # We only need a couple of fields - class FakeLogEntry(NamedTuple): - data: bytes - address: Address - topics: tuple[LogTopic, ...] - - decoded = event_filter.decode_log_entry( - FakeLogEntry( - address=address, - topics=[LogTopic(abi.uint(256).encode(1)), LogTopic(keccak(b"1234"))], - data=encode_args((abi.bytes(4), b"4567"), (abi.bytes(), b"bytestring")), - ) + log_entry = LogEntry( + address=address, + topics=(LogTopic(abi.uint(256).encode(1)), LogTopic(keccak(b"1234"))), + data=encode_args((abi.bytes(4), b"4567"), (abi.bytes(), b"bytestring")), + # These fields are not important + removed=False, + log_index=0, + transaction_index=0, + transaction_hash=TxHash(b"0" * 32), + block_hash=BlockHash(b"0" * 32), + block_number=0, ) + + decoded = event_filter.decode_log_entry(log_entry) assert decoded == dict(x=1, y=None, u=b"4567", v=b"bytestring") + log_entry = LogEntry( + address=Address(b"\xba" * 20), + topics=(), + data=b"", + # These fields are not important + removed=False, + log_index=0, + transaction_index=0, + transaction_hash=TxHash(b"0" * 32), + block_hash=BlockHash(b"0" * 32), + block_number=0, + ) + with pytest.raises(ValueError, match="Log entry originates from a different contract"): - decoded = event_filter.decode_log_entry( - FakeLogEntry(address=Address(b"\xba" * 20), topics=[], data=b"") - ) + decoded = event_filter.decode_log_entry(log_entry) diff --git a/tests/test_contract_abi.py b/tests/test_contract_abi.py index 72d575f..4fa7595 100644 --- a/tests/test_contract_abi.py +++ b/tests/test_contract_abi.py @@ -1,10 +1,10 @@ import re -from typing import NamedTuple import pytest -from ethereum_rpc import LogTopic, keccak +from ethereum_rpc import Address, BlockHash, LogEntry, LogTopic, TxHash, keccak from pons import ( + ABI_JSON, Constructor, ContractABI, Either, @@ -27,7 +27,7 @@ ) -def test_signature_from_dict(): +def test_signature_from_dict() -> None: sig = Signature(dict(a=abi.uint(8), b=abi.bool)) assert sig.canonical_form == "(uint8,bool)" assert str(sig) == "(uint8 a, bool b)" @@ -36,7 +36,7 @@ def test_signature_from_dict(): assert sig.decode_into_dict(sig.encode(b=True, a=1)) == dict(b=True, a=1) -def test_signature_from_list(): +def test_signature_from_list() -> None: sig = Signature([abi.uint(8), abi.bool]) assert str(sig) == "(uint8, bool)" assert sig.canonical_form == "(uint8,bool)" @@ -44,14 +44,14 @@ def test_signature_from_list(): assert sig.decode_into_dict(sig.encode(1, True)) == {"_0": 1, "_1": True} -def test_event_signature(): +def test_event_signature() -> None: sig = EventSignature(dict(a=abi.uint(8), b=abi.bool, c=abi.bytes(4)), {"a", "b"}) assert str(sig) == "(uint8 indexed a, bool indexed b, bytes4 c)" assert sig.canonical_form == "(uint8,bool,bytes4)" assert sig.canonical_form_nonindexed == "(bytes4)" -def test_event_signature_encode(): +def test_event_signature_encode() -> None: sig = EventSignature(dict(a=abi.uint(8), b=abi.bool, c=abi.bytes(4)), {"a", "b"}) # All indexed parameters provided @@ -74,7 +74,7 @@ def test_event_signature_encode(): ) -def test_event_signature_decode(): +def test_event_signature_decode() -> None: sig = EventSignature(dict(a=abi.uint(8), b=abi.bool, c=abi.bytes(4), d=abi.bytes()), {"a", "b"}) decoded = sig.decode_log_entry( @@ -91,7 +91,7 @@ def test_event_signature_decode(): sig.decode_log_entry([b"1", b"2", b"3"], b"zzz") -def test_constructor_from_json(): +def test_constructor_from_json() -> None: ctr = Constructor.from_json( dict( type="constructor", @@ -107,7 +107,7 @@ def test_constructor_from_json(): assert str(ctr.inputs) == "(uint8 a, bool b)" -def test_constructor_init(): +def test_constructor_init() -> None: ctr = Constructor(inputs=dict(a=abi.uint(8), b=abi.bool), payable=True) assert ctr.payable assert ctr.inputs.canonical_form == "(uint8,bool)" @@ -122,7 +122,7 @@ def test_constructor_init(): assert ctr_call.input_bytes == b"\x00" * 31 + b"\x01" + b"\x00" * 31 + b"\x01" -def test_constructor_errors(): +def test_constructor_errors() -> None: with pytest.raises( ValueError, match="Constructor object must be created from a JSON entry with type='constructor'", @@ -147,7 +147,7 @@ def test_constructor_errors(): Constructor.from_json(dict(type="constructor", stateMutability="view")) -def _check_method(method): +def _check_method(method: Method) -> None: assert method.name == "someMethod" assert method.inputs.canonical_form == "(uint8,bool)" assert str(method.inputs) == "(uint8 a, bool b)" @@ -167,7 +167,7 @@ def _check_method(method): assert call.data_bytes == method.selector + encoded_bytes -def test_method_from_json_anonymous_outputs(): +def test_method_from_json_anonymous_outputs() -> None: method = Method.from_json( dict( type="function", @@ -188,7 +188,7 @@ def test_method_from_json_anonymous_outputs(): _check_method(method) -def test_method_from_json_named_outputs(): +def test_method_from_json_named_outputs() -> None: method = Method.from_json( dict( type="function", @@ -209,7 +209,7 @@ def test_method_from_json_named_outputs(): _check_method(method) -def test_method_init(): +def test_method_init() -> None: method = Method( name="someMethod", mutability=Mutability.VIEW, @@ -221,7 +221,7 @@ def test_method_init(): _check_method(method) -def test_method_single_output(): +def test_method_single_output() -> None: method = Method( name="someMethod", mutability=Mutability.VIEW, @@ -236,13 +236,13 @@ def test_method_single_output(): assert method.decode_output(encoded_bytes) == 1 -def test_method_errors(): +def test_method_errors() -> None: with pytest.raises( ValueError, match="Method object must be created from a JSON entry with type='function'" ): Method.from_json(dict(type="constructor")) - json = dict( + json: ABI_JSON = dict( type="function", name="someMethod", stateMutability="invalid", @@ -256,7 +256,7 @@ def test_method_errors(): Method.from_json(json) -def test_multi_method(): +def test_multi_method() -> None: method1 = Method( name="someMethod", mutability=Mutability.VIEW, @@ -304,7 +304,7 @@ def test_multi_method(): multi_method(1) -def test_multi_method_errors(): +def test_multi_method_errors() -> None: with pytest.raises(ValueError, match="`methods` cannot be empty"): MultiMethod() @@ -329,12 +329,12 @@ def test_multi_method_errors(): MultiMethod(method, method) -def test_fallback(): +def test_fallback() -> None: fallback = Fallback.from_json(dict(type="fallback", stateMutability="payable")) assert fallback.payable -def test_fallback_errors(): +def test_fallback_errors() -> None: with pytest.raises( ValueError, match="Fallback object must be created from a JSON entry with type='fallback'" ): @@ -346,12 +346,12 @@ def test_fallback_errors(): Fallback.from_json(dict(type="fallback", stateMutability="view")) -def test_receive(): +def test_receive() -> None: receive = Receive.from_json(dict(type="receive", stateMutability="payable")) assert receive.payable -def test_receive_errors(): +def test_receive_errors() -> None: with pytest.raises( ValueError, match="Receive object must be created from a JSON entry with type='fallback'" ): @@ -363,8 +363,8 @@ def test_receive_errors(): Receive.from_json(dict(type="receive", stateMutability="view")) -def test_contract_abi_json(): - constructor_abi = dict( +def test_contract_abi_json() -> None: + constructor_abi: ABI_JSON = dict( type="constructor", stateMutability="payable", inputs=[ @@ -373,7 +373,7 @@ def test_contract_abi_json(): ], ) - read_abi = dict( + read_abi: ABI_JSON = dict( type="function", name="readMethod", stateMutability="view", @@ -387,7 +387,7 @@ def test_contract_abi_json(): ], ) - write_abi = dict( + write_abi: ABI_JSON = dict( type="function", name="writeMethod", stateMutability="payable", @@ -397,7 +397,7 @@ def test_contract_abi_json(): ], ) - event_abi = dict( + event_abi: ABI_JSON = dict( type="event", name="Deposit", anonymous=True, @@ -408,7 +408,7 @@ def test_contract_abi_json(): ], ) - error_abi = dict( + error_abi: ABI_JSON = dict( type="error", name="CustomError", inputs=[ @@ -418,8 +418,8 @@ def test_contract_abi_json(): ], ) - fallback_abi = dict(type="fallback", stateMutability="payable") - receive_abi = dict(type="receive", stateMutability="payable") + fallback_abi: ABI_JSON = dict(type="fallback", stateMutability="payable") + receive_abi: ABI_JSON = dict(type="receive", stateMutability="payable") cabi = ContractABI.from_json( [constructor_abi, read_abi, write_abi, fallback_abi, receive_abi, event_abi, error_abi] @@ -445,7 +445,7 @@ def test_contract_abi_json(): assert isinstance(cabi.error.CustomError, Error) -def test_contract_abi_init(): +def test_contract_abi_init() -> None: cabi = ContractABI( constructor=Constructor(inputs=dict(a=abi.uint(8), b=abi.bool), payable=True), methods=[ @@ -498,8 +498,8 @@ def test_contract_abi_init(): assert isinstance(cabi.method.writeMethod, Method) -def test_overloaded_methods(): - json_abi = [ +def test_overloaded_methods() -> None: + json_abi: ABI_JSON = [ dict( type="function", name="readMethod", @@ -537,13 +537,13 @@ def test_overloaded_methods(): assert isinstance(cabi.method.readMethod, MultiMethod) -def test_no_constructor(): +def test_no_constructor() -> None: cabi = ContractABI() assert isinstance(cabi.constructor, Constructor) assert cabi.constructor.inputs.canonical_form == "()" -def test_contract_abi_errors(): +def test_contract_abi_errors() -> None: constructor_abi = dict(type="constructor", stateMutability="payable", inputs=[]) with pytest.raises( ValueError, match="JSON ABI contains more than one constructor declarations" @@ -560,7 +560,7 @@ def test_contract_abi_errors(): ): ContractABI.from_json([receive_abi, receive_abi]) - event_abi = dict(type="event", name="Foo", inputs=[], anonymous=False) + event_abi: ABI_JSON = dict(type="event", name="Foo", inputs=[], anonymous=False) with pytest.raises(ValueError, match="JSON ABI contains more than one declarations of `Foo`"): ContractABI.from_json([event_abi, event_abi]) @@ -572,7 +572,7 @@ def test_contract_abi_errors(): ContractABI.from_json([dict(type="foobar")]) -def test_event_from_json(): +def test_event_from_json() -> None: event = Event.from_json( dict( anonymous=True, @@ -591,7 +591,7 @@ def test_event_from_json(): assert str(event.fields) == "(address indexed from_, bytes indexed foo, uint8 bar)" -def test_event_init(): +def test_event_init() -> None: event = Event( "Foo", dict(from_=abi.address, foo=abi.bytes(), bar=abi.uint(8)), @@ -604,7 +604,7 @@ def test_event_init(): assert str(event.fields) == "(address indexed from_, bytes indexed foo, uint8 bar)" -def test_event_encode(): +def test_event_encode() -> None: event = Event("Foo", dict(a=abi.bool, b=abi.uint(8), c=abi.bytes(4)), {"a", "b"}) event_filter = event(b=Either(1, 2)) assert event_filter.topics == ( @@ -624,58 +624,77 @@ def test_event_encode(): ) -def test_event_decode(): - # We only need a couple of fields - class FakeLogEntry(NamedTuple): - topics: tuple[LogTopic, ...] - data: bytes - +def test_event_decode() -> None: event = Event("Foo", dict(a=abi.bool, b=abi.uint(8), c=abi.bytes(4), d=abi.bytes()), {"a", "b"}) + entry = LogEntry( + topics=( + LogTopic(keccak(event.name.encode() + event.fields.canonical_form.encode())), + LogTopic(abi.bool.encode(True)), + LogTopic(abi.uint(8).encode(2)), + ), + data=encode_args((abi.bytes(4), b"1234"), (abi.bytes(), b"bytestring")), + # these fields do not matter for the test + address=Address(b"0" * 20), + removed=False, + log_index=0, + transaction_index=0, + transaction_hash=TxHash(b"0" * 32), + block_hash=BlockHash(b"0" * 32), + block_number=0, + ) + + decoded = event.decode_log_entry(entry) + assert decoded == dict(a=True, b=2, c=b"1234", d=b"bytestring") - decoded = event.decode_log_entry( - FakeLogEntry( - [ - LogTopic(keccak(event.name.encode() + event.fields.canonical_form.encode())), - LogTopic(abi.bool.encode(True)), - LogTopic(abi.uint(8).encode(2)), - ], - encode_args((abi.bytes(4), b"1234"), (abi.bytes(), b"bytestring")), - ) + +def test_event_decode_wrong_selector() -> None: + event = Event("Foo", dict(a=abi.bool, b=abi.uint(8), c=abi.bytes(4), d=abi.bytes()), {"a", "b"}) + entry = LogEntry( + topics=( + LogTopic(keccak(b"NotFoo" + event.fields.canonical_form.encode())), + LogTopic(abi.bool.encode(True)), + LogTopic(abi.uint(8).encode(2)), + ), + data=encode_args((abi.bytes(4), b"1234"), (abi.bytes(), b"bytestring")), + # these fields do not matter for the test + address=Address(b"0" * 20), + removed=False, + log_index=0, + transaction_index=0, + transaction_hash=TxHash(b"0" * 32), + block_hash=BlockHash(b"0" * 32), + block_number=0, ) - assert decoded == dict(a=True, b=2, c=b"1234", d=b"bytestring") - # Wrong selector with pytest.raises(ValueError, match="This log entry belongs to a different event"): - decoded = event.decode_log_entry( - FakeLogEntry( - [ - LogTopic(keccak(b"NotFoo" + event.fields.canonical_form.encode())), - LogTopic(abi.bool.encode(True)), - LogTopic(abi.uint(8).encode(2)), - ], - encode_args((abi.bytes(4), b"1234"), (abi.bytes(), b"bytestring")), - ) - ) + event.decode_log_entry(entry) - # Anonymous event +def test_event_decode_anonymous() -> None: event = Event( "Foo", dict(a=abi.bool, b=abi.uint(8), c=abi.bytes(4), d=abi.bytes()), {"a", "b"}, anonymous=True, ) - - decoded = event.decode_log_entry( - FakeLogEntry( - [LogTopic(abi.bool.encode(True)), LogTopic(abi.uint(8).encode(2))], - encode_args((abi.bytes(4), b"1234"), (abi.bytes(), b"bytestring")), - ) + entry = LogEntry( + topics=(LogTopic(abi.bool.encode(True)), LogTopic(abi.uint(8).encode(2))), + data=encode_args((abi.bytes(4), b"1234"), (abi.bytes(), b"bytestring")), + # these fields do not matter for the test + address=Address(b"0" * 20), + removed=False, + log_index=0, + transaction_index=0, + transaction_hash=TxHash(b"0" * 32), + block_hash=BlockHash(b"0" * 32), + block_number=0, ) + + decoded = event.decode_log_entry(entry) assert decoded == dict(a=True, b=2, c=b"1234", d=b"bytestring") -def test_event_errors(): +def test_event_errors() -> None: with pytest.raises( ValueError, match="Event object must be created from a JSON entry with type='event'", @@ -722,7 +741,7 @@ def test_event_errors(): ) -def test_error_from_json(): +def test_error_from_json() -> None: error = Error.from_json( dict( inputs=[ @@ -756,7 +775,7 @@ def test_error_from_json(): ) -def test_error_init(): +def test_error_init() -> None: error = Error( "Foo", dict(from_=abi.address, foo=abi.bytes(), bar=abi.uint(8)), @@ -765,7 +784,7 @@ def test_error_init(): assert str(error.fields) == "(address from_, bytes foo, uint8 bar)" -def test_error_decode(): +def test_error_decode() -> None: error = Error( "Foo", dict(foo=abi.bytes(), bar=abi.uint(8)), @@ -776,7 +795,7 @@ def test_error_decode(): assert decoded == dict(foo=b"12345", bar=9) -def test_resolve_error(): +def test_resolve_error() -> None: error1 = Error("Error1", dict(foo=abi.bytes(), bar=abi.uint(8))) error2 = Error("Error2", dict(foo=abi.bool, bar=abi.string)) contract_abi = ContractABI(errors=[error1, error2]) diff --git a/tests/test_contract_functionality.py b/tests/test_contract_functionality.py index 0413d70..e50f6a4 100644 --- a/tests/test_contract_functionality.py +++ b/tests/test_contract_functionality.py @@ -4,6 +4,9 @@ from ethereum_rpc import Amount from pons import ( + AccountSigner, + ClientSession, + CompiledContract, Constructor, ContractABI, DeployedContract, @@ -16,12 +19,16 @@ @pytest.fixture -def compiled_contracts(): +def compiled_contracts() -> dict[str, CompiledContract]: path = Path(__file__).resolve().parent / "TestContractFunctionality.sol" return compile_contract_file(path, evm_version=EVMVersion.CANCUN) -async def test_empty_constructor(session, root_signer, compiled_contracts): +async def test_empty_constructor( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: """ Checks that an empty constructor is created automatically if none is provided, and it can be used to deploy the contract. @@ -34,7 +41,12 @@ async def test_empty_constructor(session, root_signer, compiled_contracts): assert result == (1 + 123,) -async def test_basics(session, root_signer, another_signer, compiled_contracts): +async def test_basics( + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_contract = compiled_contracts["Test"] # Deploy the contract @@ -61,7 +73,11 @@ async def test_basics(session, root_signer, another_signer, compiled_contracts): assert result == (inner, outer) -async def test_overloaded_method(session, root_signer, compiled_contracts): +async def test_overloaded_method( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_contract = compiled_contracts["Test"] # Deploy the contract @@ -75,7 +91,11 @@ async def test_overloaded_method(session, root_signer, compiled_contracts): assert result == (456 + 123,) -async def test_read_only_mode(session, root_signer, compiled_contracts): +async def test_read_only_mode( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: # Test that a "nonpayable" (that is, mutating) method can still be invoked # via `eth_call`, and it will use the current state of the contract # in a "copy-on-write" mode, where it will be able to make changes, @@ -106,7 +126,12 @@ async def test_read_only_mode(session, root_signer, compiled_contracts): assert result == (value + 1,) -async def test_abi_declaration(session, root_signer, another_signer, compiled_contracts): +async def test_abi_declaration( + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_contract = compiled_contracts["Test"] # Deploy the contract @@ -155,7 +180,11 @@ async def test_abi_declaration(session, root_signer, another_signer, compiled_co assert result == (inner, outer) -async def test_complicated_event(session, root_signer, compiled_contracts): +async def test_complicated_event( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: # Smoke test for topic encoding, emitting an event with non-trivial topic structure. # The details of the encoding should be covered in ABI tests, # here we're just checking we got them right. diff --git a/tests/test_fallback_provider.py b/tests/test_fallback_provider.py index 5264cee..c73cc5d 100644 --- a/tests/test_fallback_provider.py +++ b/tests/test_fallback_provider.py @@ -4,14 +4,14 @@ from enum import Enum import pytest -from ethereum_rpc import RPCError +from ethereum_rpc import ErrorCode, RPCError from pons import CycleFallback, FallbackProvider, PriorityFallback, Unreachable from pons._fallback_provider import PriorityFallbackStrategy from pons._provider import RPC_JSON, InvalidResponse, Provider, ProviderSession -def random_request(): +def random_request() -> str: return os.urandom(2).hex() @@ -23,11 +23,11 @@ class ProviderState(Enum): class MockProvider(Provider): - def __init__(self): - self.requests = [] + def __init__(self) -> None: + self.requests: list[str] = [] self.state = ProviderState.NORMAL - def set_state(self, state: ProviderState): + def set_state(self, state: ProviderState) -> None: self.state = state @asynccontextmanager @@ -36,7 +36,7 @@ async def session(self) -> AsyncIterator["ProviderSession"]: class MockSession(ProviderSession): - def __init__(self, provider: Provider): + def __init__(self, provider: MockProvider): self.provider = provider async def rpc(self, method: str, *_args: RPC_JSON) -> RPC_JSON: @@ -46,24 +46,24 @@ async def rpc(self, method: str, *_args: RPC_JSON) -> RPC_JSON: if self.provider.state == ProviderState.BAD_RESPONSE: raise InvalidResponse("") if self.provider.state == ProviderState.RPC_ERROR: - raise RPCError(-1, "") + raise RPCError(ErrorCode(-1), "") return "success" -async def test_default_fallback(): +async def test_default_fallback() -> None: providers = [MockProvider() for i in range(3)] provider = FallbackProvider(providers) assert isinstance(provider._strategy, PriorityFallbackStrategy) -def test_inconsistent_weights_length(): +def test_inconsistent_weights_length() -> None: providers = [MockProvider() for i in range(3)] msg = r"Length of the weights \(2\) inconsistent with the number of providers \(3\)" with pytest.raises(ValueError, match=msg): FallbackProvider(providers, CycleFallback([1, 2])) -async def test_cycle_fallback(): +async def test_cycle_fallback() -> None: strategy = CycleFallback() providers = [MockProvider() for i in range(3)] provider = FallbackProvider(providers, strategy) @@ -78,7 +78,7 @@ async def test_cycle_fallback(): assert providers[2].requests == ["2", "5", "8"] -async def test_cycle_fallback_custom_weights(): +async def test_cycle_fallback_custom_weights() -> None: strategy = CycleFallback([3, 1, 2]) providers = [MockProvider() for i in range(3)] provider = FallbackProvider(providers, strategy) @@ -93,7 +93,7 @@ async def test_cycle_fallback_custom_weights(): assert providers[2].requests == ["4", "5"] -async def test_fallback_on_errors(): +async def test_fallback_on_errors() -> None: strategy = PriorityFallback() providers = [MockProvider() for i in range(3)] provider = FallbackProvider(providers, strategy) @@ -121,7 +121,7 @@ async def test_fallback_on_errors(): assert providers[2].requests[-1] == request -async def test_raising_errors(): +async def test_raising_errors() -> None: strategy = PriorityFallback() providers = [MockProvider() for i in range(3)] provider = FallbackProvider(providers, strategy) @@ -149,7 +149,7 @@ async def test_raising_errors(): await session.rpc(random_request()) -async def test_nested_providers(): +async def test_nested_providers() -> None: providers = [MockProvider() for i in range(4)] subprovider1 = FallbackProvider([providers[0], providers[1]], PriorityFallback()) subprovider2 = FallbackProvider( @@ -197,7 +197,7 @@ async def test_nested_providers(): assert providers[3].requests[-1] == request -async def test_invalid_path(): +async def test_invalid_path() -> None: providers = [MockProvider() for i in range(4)] subprovider1 = FallbackProvider([providers[0], providers[1]], PriorityFallback()) subprovider2 = FallbackProvider( diff --git a/tests/test_http_provider_server.py b/tests/test_http_provider_server.py index 842b66f..4833e14 100644 --- a/tests/test_http_provider_server.py +++ b/tests/test_http_provider_server.py @@ -1,14 +1,18 @@ -# TODO (#60): expand the tests so that this file covered 100% of the respective submodule, -# and don't use high-level API. +from collections.abc import AsyncIterator +from typing import Any import pytest +import trio from ethereum_rpc import RPCError, RPCErrorCode -from pons import HTTPProviderServer +from pons import HTTPProviderServer, InvalidResponse, LocalProvider +from pons._provider import RPC_JSON, ProviderSession @pytest.fixture -async def server(nursery, local_provider): +async def server( + nursery: trio.Nursery, local_provider: LocalProvider +) -> AsyncIterator[HTTPProviderServer]: handle = HTTPProviderServer(local_provider) await nursery.start(handle) yield handle @@ -16,18 +20,25 @@ async def server(nursery, local_provider): @pytest.fixture -async def provider_session(server): +async def provider_session(server: HTTPProviderServer) -> AsyncIterator[ProviderSession]: async with server.http_provider.session() as session: yield session -async def test_invalid_method(provider_session): +async def test_happy_path(provider_session: ProviderSession) -> None: + result = await provider_session.rpc("eth_chainId") + assert result == "0x1" + + +async def test_invalid_method(provider_session: ProviderSession) -> None: with pytest.raises(RPCError) as excinfo: - await provider_session.rpc(["method1", "method2"], 1, 2, 3) + await provider_session.rpc(["method1", "method2"], 1, 2, 3) # type: ignore[arg-type] assert excinfo.value.code == RPCErrorCode.INVALID_REQUEST.value -async def test_invalid_parameters(provider_session, monkeypatch): +async def test_invalid_parameters( + provider_session: ProviderSession, monkeypatch: pytest.MonkeyPatch +) -> None: # There is no public way to do that, have to use the internals monkeypatch.setattr( provider_session, @@ -39,3 +50,16 @@ async def test_invalid_parameters(provider_session, monkeypatch): with pytest.raises(RPCError) as excinfo: await provider_session.rpc("method1", 1, 2, 3) assert excinfo.value.code == RPCErrorCode.INVALID_REQUEST.value + + +async def test_internal_error( + provider_session: ProviderSession, + local_provider: LocalProvider, + monkeypatch: pytest.MonkeyPatch, +) -> None: + def mock_rpc(*_args: Any) -> RPC_JSON: + raise RuntimeError("foo") + + monkeypatch.setattr(local_provider, "rpc", mock_rpc) + with pytest.raises(InvalidResponse, match="Expected a JSON response, got HTTP status 500: foo"): + await provider_session.rpc("eth_chainId") diff --git a/tests/test_local_provider.py b/tests/test_local_provider.py index 37a5c09..db13e4c 100644 --- a/tests/test_local_provider.py +++ b/tests/test_local_provider.py @@ -1,36 +1,36 @@ -# TODO (#60): expand the tests so that this file covered 100% of the respective submodule. +from collections.abc import AsyncIterator import pytest from ethereum_rpc import Amount, RPCError -from pons import AccountSigner, Client, LocalProvider +from pons import AccountSigner, Client, ClientSession, LocalProvider # Masking the global fixtures to make this test self-contained # (since LocalProvider is what we're testing). @pytest.fixture -def provider(): +def provider() -> LocalProvider: return LocalProvider(root_balance=Amount.ether(100)) @pytest.fixture -async def session(provider): +async def session(provider: LocalProvider) -> AsyncIterator[ClientSession]: client = Client(provider=provider) async with client.session() as session: yield session @pytest.fixture -def root_signer(provider): +def root_signer(provider: LocalProvider) -> AccountSigner: return provider.root @pytest.fixture -def another_signer(): +def another_signer() -> AccountSigner: return AccountSigner.create() -async def test_root_balance(): +async def test_root_balance() -> None: amount = Amount.ether(123) provider = LocalProvider(root_balance=amount) client = Client(provider=provider) @@ -38,13 +38,19 @@ async def test_root_balance(): assert await session.get_balance(provider.root.address) == amount -async def test_auto_mine(provider, session, root_signer, another_signer): +async def test_auto_mine( + provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: amount = Amount.ether(1) dest = another_signer.address # Auto-mininig is the default behavior tx_hash = await session.broadcast_transfer(root_signer, dest, amount) receipt = await session.rpc.eth_get_transaction_receipt(tx_hash) + assert receipt is not None assert receipt.succeeded assert await session.get_balance(dest) == amount @@ -58,11 +64,17 @@ async def test_auto_mine(provider, session, root_signer, another_signer): # Enable auto-mining back. The pending transactions are added to the block. provider.enable_auto_mine_transactions() receipt = await session.rpc.eth_get_transaction_receipt(tx_hash) + assert receipt is not None assert receipt.succeeded assert await session.get_balance(dest) == Amount.ether(2) -async def test_snapshots(provider, session, root_signer, another_signer): +async def test_snapshots( + provider: LocalProvider, + session: ClientSession, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: amount = Amount.ether(1) double_amount = Amount.ether(2) dest = another_signer.address @@ -76,15 +88,15 @@ async def test_snapshots(provider, session, root_signer, another_signer): assert await session.get_balance(dest) == amount -async def test_net_version(session): +async def test_net_version(session: ClientSession) -> None: assert await session.net_version() == "1" -def test_rpc_error(provider): +def test_rpc_error(provider: LocalProvider) -> None: with pytest.raises(RPCError, match="Unknown method: eth_nonexistentMethod"): provider.rpc("eth_nonexistentMethod") -def test_eth_chain_id(): +def test_eth_chain_id() -> None: provider = LocalProvider(root_balance=Amount.ether(100), chain_id=0xABC) assert provider.rpc("eth_chainId") == "0xabc" diff --git a/tests/test_multicall.py b/tests/test_multicall.py index 13cab9d..3d031a9 100644 --- a/tests/test_multicall.py +++ b/tests/test_multicall.py @@ -3,41 +3,60 @@ import pytest from ethereum_rpc import Amount -from pons import ContractLegacyError, Multicall, compile_contract_file +from pons import ( + AccountSigner, + ClientSession, + CompiledContract, + ContractLegacyError, + DeployedContract, + Multicall, + compile_contract_file, +) @pytest.fixture -def multicall_compiled(): +def multicall_compiled() -> CompiledContract: path = Path(__file__).resolve().parent / "Multicall3.sol" compiled = compile_contract_file(path) return compiled["Multicall3"] @pytest.fixture -def target_compiled(): +def target_compiled() -> CompiledContract: path = Path(__file__).resolve().parent / "TestMulticall.sol" compiled = compile_contract_file(path) return compiled["TestMulticall"] @pytest.fixture -async def multicall(session, root_signer, multicall_compiled): +async def multicall( + session: ClientSession, root_signer: AccountSigner, multicall_compiled: CompiledContract +) -> Multicall: multicall_deployed = await session.deploy(root_signer, multicall_compiled.constructor()) return Multicall(multicall_deployed.address) @pytest.fixture -async def target(session, root_signer, target_compiled): +async def target( + session: ClientSession, root_signer: AccountSigner, target_compiled: CompiledContract +) -> DeployedContract: return await session.deploy(root_signer, target_compiled.constructor()) -async def test_multicall_read(session, multicall, target): +async def test_multicall_read( + session: ClientSession, multicall: Multicall, target: DeployedContract +) -> None: results = await session.call(multicall.aggregate([target.method.x1(), target.method.x2()])) assert results[0] == (True, (1,)) assert results[1] == (True, (2,)) -async def test_multicall_write(session, root_signer, multicall, target): +async def test_multicall_write( + session: ClientSession, + root_signer: AccountSigner, + multicall: Multicall, + target: DeployedContract, +) -> None: await session.transact( root_signer, multicall.aggregate([target.method.write1(3), target.method.write2(4)]) ) @@ -45,7 +64,9 @@ async def test_multicall_write(session, root_signer, multicall, target): assert await session.call(target.method.x2()) == (4,) -async def test_multicall_read_error(session, multicall, target): +async def test_multicall_read_error( + session: ClientSession, multicall: Multicall, target: DeployedContract +) -> None: # For now `eth_call` does not decode contract errors, so we get the low-level one. with pytest.raises(ContractLegacyError, match="Multicall3: call failed"): await session.call( @@ -59,7 +80,12 @@ async def test_multicall_read_error(session, multicall, target): assert results[1] == (False, ()) # errored call -async def test_multicall_write_error(session, root_signer, multicall, target): +async def test_multicall_write_error( + session: ClientSession, + root_signer: AccountSigner, + multicall: Multicall, + target: DeployedContract, +) -> None: with pytest.raises(ContractLegacyError, match="Multicall3: call failed"): await session.transact( root_signer, @@ -84,7 +110,9 @@ async def test_multicall_write_error(session, root_signer, multicall, target): assert await session.call(target.method.x2()) == (2,) -async def test_multicall_read_value(session, multicall, target): +async def test_multicall_read_value( + session: ClientSession, multicall: Multicall, target: DeployedContract +) -> None: results = await session.call( multicall.aggregate_value( [(target.method.x1(), Amount.wei(0)), (target.method.x2(), Amount.wei(0))] @@ -94,7 +122,12 @@ async def test_multicall_read_value(session, multicall, target): assert results[1] == (True, (2,)) -async def test_multicall_write_value(session, root_signer, multicall, target): +async def test_multicall_write_value( + session: ClientSession, + root_signer: AccountSigner, + multicall: Multicall, + target: DeployedContract, +) -> None: await session.transact( root_signer, multicall.aggregate_value( @@ -109,7 +142,12 @@ async def test_multicall_write_value(session, root_signer, multicall, target): assert await session.call(target.method.x2()) == (200,) -async def test_multicall_write_value_insufficient_funds(session, root_signer, multicall, target): +async def test_multicall_write_value_insufficient_funds( + session: ClientSession, + root_signer: AccountSigner, + multicall: Multicall, + target: DeployedContract, +) -> None: with pytest.raises(ContractLegacyError, match="Multicall3: call failed"): await session.transact( root_signer, diff --git a/tests/test_provider.py b/tests/test_provider.py index b1c422b..fcebadc 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -1,23 +1,34 @@ +from collections.abc import AsyncIterator from contextlib import asynccontextmanager from http import HTTPStatus +from typing import Any import pytest import trio from ethereum_rpc import Amount +from pytest import MonkeyPatch from pons import ( + AccountSigner, + BadResponseFormat, Client, + ClientSession, + HTTPError, HTTPProvider, HTTPProviderServer, + LocalProvider, + Provider, + ProviderError, Unreachable, _http_provider_server, # For monkeypatching purposes ) -from pons._client import BadResponseFormat, ProviderError -from pons._provider import HTTPError, Provider, ProviderSession +from pons._provider import RPC_JSON, ProviderSession @pytest.fixture -async def test_server(nursery, local_provider): +async def test_server( + nursery: trio.Nursery, local_provider: LocalProvider +) -> AsyncIterator[HTTPProviderServer]: handle = HTTPProviderServer(local_provider) await nursery.start(handle) yield handle @@ -25,21 +36,25 @@ async def test_server(nursery, local_provider): @pytest.fixture -async def session(test_server): +async def session(test_server: HTTPProviderServer) -> AsyncIterator[ClientSession]: client = Client(test_server.http_provider) async with client.session() as session: yield session -async def test_single_value_request(session): +async def test_single_value_request(session: ClientSession) -> None: assert await session.net_version() == "1" -async def test_dict_request(session, root_signer, another_signer): +async def test_dict_request( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: await session.transfer(root_signer, another_signer.address, Amount.ether(10)) -async def test_dict_request_introspection(session, root_signer, another_signer): +async def test_dict_request_introspection( + session: ClientSession, root_signer: AccountSigner, another_signer: AccountSigner +) -> None: # This test covers the __contains__ method of ResponseDict. # It is invoked when the error response is checked for the "data" field, # so we trigger an intentionally bad transaction. @@ -54,8 +69,12 @@ async def test_dict_request_introspection(session, root_signer, another_signer): async def test_unexpected_response_type( - local_provider, session, monkeypatch, root_signer, another_signer -): + local_provider: LocalProvider, + session: ClientSession, + monkeypatch: MonkeyPatch, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: tx_hash = await session.broadcast_transfer( root_signer, another_signer.address, Amount.ether(10) ) @@ -66,12 +85,19 @@ async def test_unexpected_response_type( await session.rpc.eth_get_transaction_receipt(tx_hash) -async def test_missing_field(local_provider, session, monkeypatch, root_signer, another_signer): +async def test_missing_field( + local_provider: LocalProvider, + session: ClientSession, + monkeypatch: MonkeyPatch, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: orig_rpc = local_provider.rpc - def mock_rpc(method, *args): + def mock_rpc(method: str, *args: Any) -> RPC_JSON: result = orig_rpc(method, *args) if method == "eth_getTransactionReceipt": + assert isinstance(result, dict) del result["status"] return result @@ -86,8 +112,12 @@ def mock_rpc(method, *args): async def test_none_instead_of_dict( - local_provider, session, monkeypatch, root_signer, another_signer -): + local_provider: LocalProvider, + session: ClientSession, + monkeypatch: MonkeyPatch, + root_signer: AccountSigner, + another_signer: AccountSigner, +) -> None: tx_hash = await session.broadcast_transfer( root_signer, another_signer.address, Amount.ether(10) ) @@ -101,8 +131,10 @@ async def test_none_instead_of_dict( assert await session.rpc.eth_get_transaction_receipt(tx_hash) is None -async def test_non_json_response(local_provider, session, monkeypatch): - def faulty_net_version(_method, *_args): +async def test_non_json_response( + local_provider: LocalProvider, session: ClientSession, monkeypatch: MonkeyPatch +) -> None: + def faulty_net_version(_method: str, *_args: Any) -> RPC_JSON: # A generic exception will generate a 500 status code raise Exception("Something unexpected happened") # noqa: TRY002 @@ -113,13 +145,14 @@ def faulty_net_version(_method, *_args): await session.net_version() -async def test_no_result_field(session, monkeypatch): +async def test_no_result_field(session: ClientSession, monkeypatch: MonkeyPatch) -> None: # Tests the handling of a badly formed success response without the "result" field. orig_process_request = _http_provider_server.process_request - async def faulty_process_request(*args, **kwargs): + async def faulty_process_request(*args: Any, **kwargs: Any) -> tuple[HTTPStatus, RPC_JSON]: status, response = await orig_process_request(*args, **kwargs) + assert isinstance(response, dict) del response["result"] return (status, response) @@ -129,13 +162,14 @@ async def faulty_process_request(*args, **kwargs): await session.net_version() -async def test_no_error_field(session, monkeypatch): +async def test_no_error_field(session: ClientSession, monkeypatch: MonkeyPatch) -> None: # Tests the handling of a badly formed error response without the "error" field. orig_process_request = _http_provider_server.process_request - async def faulty_process_request(*args, **kwargs): + async def faulty_process_request(*args: Any, **kwargs: Any) -> tuple[HTTPStatus, RPC_JSON]: _status, response = await orig_process_request(*args, **kwargs) + assert isinstance(response, dict) del response["result"] return (HTTPStatus.BAD_REQUEST, response) @@ -145,14 +179,15 @@ async def faulty_process_request(*args, **kwargs): await session.net_version() -async def test_malformed_error_field(session, monkeypatch): +async def test_malformed_error_field(session: ClientSession, monkeypatch: MonkeyPatch) -> None: # Tests the handling of a badly formed error response # where the "error" field cannot be parsed as an RPCError. orig_process_request = _http_provider_server.process_request - async def faulty_process_request(*args, **kwargs): + async def faulty_process_request(*args: Any, **kwargs: Any) -> RPC_JSON: _status, response = await orig_process_request(*args, **kwargs) + assert isinstance(response, dict) del response["result"] response["error"] = {"something_weird": 1} return (HTTPStatus.BAD_REQUEST, response) @@ -163,11 +198,11 @@ async def faulty_process_request(*args, **kwargs): await session.net_version() -async def test_result_is_not_a_dict(session, monkeypatch): +async def test_result_is_not_a_dict(session: ClientSession, monkeypatch: MonkeyPatch) -> None: # Tests the handling of a badly formed provider response that is not a dictionary. # Unfortunately we can't achieve that by just patching the provider, have to patch the server - async def faulty_process_request(*_args, **_kwargs): + async def faulty_process_request(*_args: Any, **_kwargs: Any) -> tuple[HTTPStatus, RPC_JSON]: return (HTTPStatus.OK, 1) monkeypatch.setattr(_http_provider_server, "process_request", faulty_process_request) @@ -176,7 +211,7 @@ async def faulty_process_request(*_args, **_kwargs): await session.net_version() -async def test_unreachable_provider(): +async def test_unreachable_provider() -> None: bad_provider = HTTPProvider("https://127.0.0.1:8889") client = Client(bad_provider) async with client.session() as session: @@ -187,23 +222,23 @@ async def test_unreachable_provider(): await session.net_version() -async def test_default_implementations(): +async def test_default_implementations() -> None: class MockProvider(Provider): @asynccontextmanager - async def session(self): + async def session(self) -> "AsyncIterator[MockSession]": yield MockSession() class MockSession(ProviderSession): - async def rpc(self, method, *_args): + async def rpc(self, method: str, *_args: RPC_JSON) -> RPC_JSON: return method provider = MockProvider() async with provider.session() as session: - result = await session.rpc_and_pin("1") - assert result == ("1", ()) + result1 = await session.rpc_and_pin("1") + assert result1 == ("1", ()) - result = await session.rpc_at_pin((), "2") - assert result == "2" + result2 = await session.rpc_at_pin((), "2") + assert result2 == "2" with pytest.raises(ValueError, match=r"Unexpected provider path: \(1,\)"): await session.rpc_at_pin((1,), "3") diff --git a/tests/test_signer.py b/tests/test_signer.py index f2bb347..0bb8c11 100644 --- a/tests/test_signer.py +++ b/tests/test_signer.py @@ -1,11 +1,17 @@ from eth_account import Account +from eth_account.types import HexStr, TransactionDictType from ethereum_rpc import Address from pons import AccountSigner -def check_signer(signer): - tx = dict(gas="0x3333", gasPrice="0x4444", nonce="0x5555", value="0x6666") +def check_signer(signer: AccountSigner) -> None: + tx: TransactionDictType = dict( + gas=HexStr("0x3333"), + gasPrice=HexStr("0x4444"), + nonce=HexStr("0x5555"), + value=HexStr("0x6666"), + ) sig = signer.sign_transaction(tx) assert isinstance(sig, bytes) # the length may vary depending on the integers in the signature (they're not padded) @@ -28,7 +34,7 @@ def check_signer(signer): assert sig.startswith(bytes.fromhex(f"f8{payload_length:x}8255558244448233338082666680")) -def test_signer(): +def test_signer() -> None: acc = Account.create() signer = AccountSigner(acc) @@ -39,6 +45,6 @@ def test_signer(): check_signer(signer) -def test_random_signer(): +def test_random_signer() -> None: signer = AccountSigner.create() check_signer(signer) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4c6617f..77e1567 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,17 +3,28 @@ import pytest -from pons import EVMVersion, compile_contract_file, get_create2_address +from pons import ( + AccountSigner, + ClientSession, + CompiledContract, + EVMVersion, + compile_contract_file, + get_create2_address, +) from pons._utils import get_create_address @pytest.fixture -def compiled_contracts(): +def compiled_contracts() -> dict[str, CompiledContract]: path = Path(__file__).resolve().parent / "TestUtils.sol" return compile_contract_file(path, evm_version=EVMVersion.CANCUN) -async def test_create(session, root_signer, compiled_contracts): +async def test_create( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_to_deploy = compiled_contracts["ToDeploy"] # Try deploying the contract with different nonces @@ -34,7 +45,11 @@ async def test_create(session, root_signer, compiled_contracts): assert to_deploy.address == get_create_address(root_signer.address, nonce) -async def test_create2(session, root_signer, compiled_contracts): +async def test_create2( + session: ClientSession, + root_signer: AccountSigner, + compiled_contracts: dict[str, CompiledContract], +) -> None: compiled_deployer = compiled_contracts["Create2Deployer"] compiled_to_deploy = compiled_contracts["ToDeploy"]