Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/lints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ Errors
.. autoclass:: pons.ABIDecodingError
:show-inheritance:

.. autoclass:: pons.InvalidResponse
:show-inheritance:

.. autoclass:: pons.Unreachable
:show-inheritance:

Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion pons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -90,6 +97,7 @@
"HTTPError",
"HTTPProvider",
"HTTPProviderServer",
"InvalidResponse",
"LocalProvider",
"Method",
"MethodCall",
Expand Down
4 changes: 2 additions & 2 deletions pons/_local_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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__(
Expand Down
11 changes: 4 additions & 7 deletions pons/_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 7 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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()
56 changes: 32 additions & 24 deletions tests/test_abi_types.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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)

Expand All @@ -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"

Expand All @@ -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

Expand All @@ -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]
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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"),
Expand All @@ -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]"),
Expand All @@ -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]),
]
Expand All @@ -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],
Expand All @@ -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
Expand All @@ -288,15 +294,17 @@ 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
else:
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(
Expand Down Expand Up @@ -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),
Expand All @@ -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))
Expand All @@ -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

Expand Down
Loading