diff --git a/docs/api.rst b/docs/api.rst index 458616f..306be39 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -13,6 +13,9 @@ Clients .. autoclass:: ClientSession() :members: +.. autoclass:: ClientSessionRPC() + :members: + Providers --------- @@ -46,14 +49,22 @@ Errors ------ .. autoclass:: pons.ABIDecodingError - -.. autoclass:: pons.RemoteError + :show-inheritance: .. autoclass:: pons.Unreachable + :show-inheritance: .. autoclass:: pons.ProtocolError + :show-inheritance: + +.. autoclass:: pons.HTTPError + :show-inheritance: .. autoclass:: pons.TransactionFailed + :show-inheritance: + +.. autoclass:: pons.BadResponseFormat + :show-inheritance: .. autoclass:: pons.ProviderError() :show-inheritance: @@ -252,11 +263,11 @@ Compiled and deployed contracts Filter objects -------------- -.. autoclass:: pons._client.BlockFilter() +.. autoclass:: pons._client_rpc.BlockFilter() -.. autoclass:: pons._client.PendingTransactionFilter() +.. autoclass:: pons._client_rpc.PendingTransactionFilter() -.. autoclass:: pons._client.LogFilter() +.. autoclass:: pons._client_rpc.LogFilter() Solidity types diff --git a/docs/changelog.rst b/docs/changelog.rst index 193291f..6cff41d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ Changed - ``JSON`` removed from the public API, instead we have a more specific ``ABI_JSON``. (PR_82_) - Renamed ``id_`` fields of ``BlockFilter``, ``PendingTransactionFilter``, and ``LogFilter`` to just ``id``. (PR_82_) - Split out ``http-provider-server`` feature from ``local-provider``. (PR_82_) +- ``RemoteError`` removed. (PR_82_) Added @@ -19,6 +20,7 @@ Added - Hash methods for ABI types. (PR_81_) - 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_) .. _PR_81: https://github.com/fjarri-eth/pons/pull/81 diff --git a/docs/index.rst b/docs/index.rst index 60d92f0..8151170 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -91,13 +91,13 @@ A quick usage example: signer = AccountSigner(acc) async with client.session() as session: - my_balance = await session.eth_get_balance(signer.address) + my_balance = await session.get_balance(signer.address) print("My balance:", my_balance.as_ether(), "ETH") another_address = Address.from_hex("0x") await session.transfer(signer, another_address, Amount.ether(1.5)) - another_balance = await session.eth_get_balance(another_address) + another_balance = await session.get_balance(another_address) print("Another balance:", another_balance.as_ether(), "ETH") trio.run(main) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index ac952ad..7d49c71 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -178,12 +178,12 @@ Interacting with deployed contracts A :py:class:`~pons.DeployedContract` object wraps all ABI method objects into "bound" state, similarly to how Python methods are bound to class instances. It means that all the method calls created from this object have the contract address inside them, so that it does not need to be provided every time. -For example, to call a non-mutating contract method via :py:meth:`~pons.ClientSession.eth_call`: +For example, to call a non-mutating contract method via :py:meth:`~pons.ClientSession.call`: :: call = deployed_contract.method.getState(1) - result = await session.eth_call(call) + result = await session.call(call) Note that when the :py:class:`~pons.ContractABI` object is created from the JSON ABI, even if the method returns a single value, it is still represented as a list of one element in the JSON, so the ``result`` will be a list too. If the ABI is declared programmatically, one can provide a single output value instead of the list, and then ``pons`` will unpack that list. diff --git a/pons/__init__.py b/pons/__init__.py index 254ff13..c568d0c 100644 --- a/pons/__init__.py +++ b/pons/__init__.py @@ -8,10 +8,9 @@ ContractError, ContractLegacyError, ContractPanic, - ProviderError, - RemoteError, TransactionFailed, ) +from ._client_rpc import BadResponseFormat, ClientSessionRPC, ProviderError from ._compiler import EVMVersion, compile_contract_file from ._contract import ( BaseBoundMethodCall, @@ -49,7 +48,7 @@ from ._http_provider_server import HTTPProviderServer from ._local_provider import LocalProvider, SnapshotID from ._multicall import BoundMultiMethodCall, BoundMultiMethodValueCall, Multicall -from ._provider import HTTPProvider, ProtocolError, Provider, Unreachable +from ._provider import HTTPError, HTTPProvider, ProtocolError, Provider, Unreachable from ._signer import AccountSigner, Signer from ._utils import get_create2_address, get_create_address @@ -57,6 +56,7 @@ "ABI_JSON", "ABIDecodingError", "AccountSigner", + "BadResponseFormat", "BaseBoundMethodCall", "BoundConstructor", "BoundConstructorCall", @@ -68,6 +68,7 @@ "BoundMultiMethodValueCall", "Client", "ClientSession", + "ClientSessionRPC", "CompiledContract", "Constructor", "ConstructorCall", @@ -86,6 +87,7 @@ "FallbackProvider", "FallbackStrategy", "FallbackStrategyFactory", + "HTTPError", "HTTPProvider", "HTTPProviderServer", "LocalProvider", @@ -99,7 +101,6 @@ "Provider", "ProviderError", "Receive", - "RemoteError", "Signer", "SnapshotID", "TransactionFailed", diff --git a/pons/_client.py b/pons/_client.py index 796e286..3262456 100644 --- a/pons/_client.py +++ b/pons/_client.py @@ -1,11 +1,9 @@ -from collections.abc import AsyncIterator, Iterable, Iterator, Sequence -from contextlib import asynccontextmanager, contextmanager -from dataclasses import dataclass +from collections.abc import AsyncIterator, Sequence +from contextlib import asynccontextmanager from enum import Enum -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast +from typing import TYPE_CHECKING, Any, ParamSpec, cast import anyio -from compages import StructuringError from ethereum_rpc import ( Address, Amount, @@ -14,19 +12,16 @@ BlockInfo, BlockLabel, EstimateGasParams, - EthCallParams, - FilterParams, LogEntry, - RPCError, RPCErrorCode, TxHash, TxInfo, TxReceipt, Type2Transaction, - structure, unstructure, ) +from ._client_rpc import BadResponseFormat, ClientSessionRPC, ProviderError from ._contract import ( BaseBoundMethodCall, BoundConstructorCall, @@ -42,31 +37,13 @@ EventFilter, UnknownError, ) -from ._provider import InvalidResponse, Provider, ProviderSession +from ._provider import Provider, ProviderSession from ._signer import Signer if TYPE_CHECKING: # pragma: no cover from eth_account.types import TransactionDictType -@dataclass -class BlockFilter: - id: int - provider_path: tuple[int, ...] - - -@dataclass -class PendingTransactionFilter: - id: int - provider_path: tuple[int, ...] - - -@dataclass -class LogFilter: - id: int - provider_path: tuple[int, ...] - - class Client: """An Ethereum RPC client.""" @@ -84,108 +61,14 @@ async def session(self) -> AsyncIterator["ClientSession"]: # TODO (#58): incorporate cached values from the session back into the client -class RemoteError(Exception): - """ - A base of all errors occurring on the provider's side. - Encompasses both errors returned via HTTP status codes - and the ones returned via the JSON response. - """ - - -class BadResponseFormat(RemoteError): - """Raised if the RPC provider returned an unexpectedly formatted response.""" - - -class TransactionFailed(RemoteError): +class TransactionFailed(Exception): """ - Raised if the transaction was submitted successfully, - but the final receipt indicates a failure. + Raised for invalid transactions that are not contract executions + (e.g. transfers or contract deployments). """ -class ProviderError(RemoteError): - """A general problem with fulfilling the request at the provider's side.""" - - raw_code: int - """The error code returned by the server.""" - - code: None | RPCErrorCode - """The parsed error code (if known).""" - - message: str - """The error message.""" - - data: None | bytes - """The associated data (if any).""" - - @classmethod - def from_rpc_error(cls, exc: RPCError) -> "ProviderError": - return cls(exc.code, exc.parsed_code, exc.message, exc.data) - - def __init__( - self, raw_code: int, code: None | RPCErrorCode, message: str, data: None | bytes = None - ): - super().__init__(raw_code, code, message, data) - self.raw_code = raw_code - self.code = code - self.message = message - self.data = data - - def __str__(self) -> str: - # Substitute the known code if any, or report the raw integer value otherwise - code = self.code or self.raw_code - return f"Provider error ({code}): {self.message}" + ( - f" (data: {self.data.hex()})" if self.data else "" - ) - - Param = ParamSpec("Param") -RetType = TypeVar("RetType") - - -@contextmanager -def convert_errors(method_name: str) -> Iterator[None]: - try: - yield - except (StructuringError, InvalidResponse) as exc: - raise BadResponseFormat(f"{method_name}: {exc}") from exc - except RPCError as exc: - raise ProviderError.from_rpc_error(exc) from exc - - -async def rpc_call( - provider_session: ProviderSession, method_name: str, ret_type: type[RetType], *args: Any -) -> RetType: - """Catches various response formatting errors and returns them in a unified way.""" - with convert_errors(method_name): - result = await provider_session.rpc(method_name, *(unstructure(arg) for arg in args)) - return structure(ret_type, result) - - -async def rpc_call_pin( - provider_session: ProviderSession, method_name: str, ret_type: type[RetType], *args: Any -) -> tuple[RetType, tuple[int, ...]]: - """Catches various response formatting errors and returns them in a unified way.""" - with convert_errors(method_name): - result, provider_path = await provider_session.rpc_and_pin( - method_name, *(unstructure(arg) for arg in args) - ) - return structure(ret_type, result), provider_path - - -async def rpc_call_at_pin( - provider_session: ProviderSession, - provider_path: tuple[int, ...], - method_name: str, - ret_type: type[RetType], - *args: Any, -) -> RetType: - """Catches various response formatting errors and returns them in a unified way.""" - with convert_errors(method_name): - result = await provider_session.rpc_at_pin( - provider_path, method_name, *(unstructure(arg) for arg in args) - ) - return structure(ret_type, result) class ContractPanicReason(Enum): @@ -238,7 +121,7 @@ def from_int(cls, val: int) -> "ContractPanicReason": return cls.UNKNOWN -class ContractPanic(RemoteError): +class ContractPanic(Exception): """A panic raised in a contract call.""" Reason = ContractPanicReason @@ -255,7 +138,7 @@ def __init__(self, reason: ContractPanicReason): self.reason = reason -class ContractLegacyError(RemoteError): +class ContractLegacyError(Exception): """A raised Solidity legacy error (from ``require()`` or ``revert()``).""" message: str @@ -266,7 +149,7 @@ def __init__(self, message: str): self.message = message -class ContractError(RemoteError): +class ContractError(Exception): """A raised Solidity error (from ``revert SomeError(...)``).""" error: Error @@ -303,97 +186,71 @@ def decode_contract_error( class ClientSession: - """An open session to the provider.""" + """ + An open session to the provider. + + The methods of this class may raise the following exceptions: + :py:class:`ProviderError`, + :py:class:`ContractLegacyError`, + :py:class:`ContractError`, + :py:class:`ContractPanic`, + :py:class:`TransactionFailed`, + :py:class:`Unreachable`, + :py:class:`BadResponseFormat`, + a provider-specific derived class of :py:class:`ProtocolError`. + """ def __init__(self, provider_session: ProviderSession): self._provider_session = provider_session self._net_version: None | str = None self._chain_id: None | int = None + self._rpc = ClientSessionRPC(self._provider_session) + + @property + def rpc(self) -> ClientSessionRPC: + return ClientSessionRPC(self._provider_session) async def net_version(self) -> str: """Calls the ``net_version`` RPC method.""" if self._net_version is None: - self._net_version = await rpc_call(self._provider_session, "net_version", str) + self._net_version = await self.rpc.net_version() return self._net_version - async def eth_chain_id(self) -> int: + async def chain_id(self) -> int: """Calls the ``eth_chainId`` RPC method.""" if self._chain_id is None: - self._chain_id = await rpc_call(self._provider_session, "eth_chainId", int) + self._chain_id = await self.rpc.eth_chain_id() return self._chain_id - async def eth_get_balance(self, address: Address, block: Block = BlockLabel.LATEST) -> Amount: - """Calls the ``eth_getBalance`` RPC method.""" - return await rpc_call(self._provider_session, "eth_getBalance", Amount, address, block) - - async def eth_get_transaction_by_hash(self, tx_hash: TxHash) -> None | TxInfo: - """Calls the ``eth_getTransactionByHash`` RPC method.""" - # Need an explicit cast, mypy doesn't work with union types correctly. - # See https://github.com/python/mypy/issues/16935 - return cast( - "None | TxInfo", - await rpc_call( - self._provider_session, - "eth_getTransactionByHash", - None | TxInfo, # type: ignore[arg-type] - tx_hash, - ), - ) - - async def eth_get_transaction_receipt(self, tx_hash: TxHash) -> None | TxReceipt: - """Calls the ``eth_getTransactionReceipt`` RPC method.""" - # Need an explicit cast, mypy doesn't work with union types correctly. - # See https://github.com/python/mypy/issues/16935 - return cast( - "None | TxReceipt", - await rpc_call( - self._provider_session, - "eth_getTransactionReceipt", - None | TxReceipt, # type: ignore[arg-type] - tx_hash, - ), - ) - - async def eth_get_transaction_count( - self, address: Address, block: Block = BlockLabel.LATEST - ) -> int: - """Calls the ``eth_getTransactionCount`` RPC method.""" - return await rpc_call( - self._provider_session, - "eth_getTransactionCount", - int, - address, - block, - ) - - async def eth_get_code(self, address: Address, block: Block = BlockLabel.LATEST) -> bytes: - """Calls the ``eth_getCode`` RPC method.""" - return await rpc_call(self._provider_session, "eth_getCode", bytes, address, block) - - async def eth_get_storage_at( - self, address: Address, position: int, block: Block = BlockLabel.LATEST - ) -> bytes: - """Calls the ``eth_getCode`` RPC method.""" - return await rpc_call( - self._provider_session, - "eth_getStorageAt", - bytes, - address, - position, - block, - ) - async def wait_for_transaction_receipt( self, tx_hash: TxHash, poll_latency: float = 1.0 ) -> TxReceipt: """Queries the transaction receipt waiting for ``poll_latency`` between each attempt.""" while True: - receipt = await self.eth_get_transaction_receipt(tx_hash) + receipt = await self._rpc.eth_get_transaction_receipt(tx_hash) if receipt is not None: return receipt await anyio.sleep(poll_latency) - async def eth_call( + async def get_balance(self, address: Address, block: Block = BlockLabel.LATEST) -> Amount: + """Query the balance of ``address`` at ``block``.""" + return await self._rpc.eth_get_balance(address, block=block) + + async def get_transaction(self, tx_hash: TxHash) -> None | TxInfo: + return await self._rpc.eth_get_transaction_by_hash(tx_hash) + + async def get_block( + self, block_id: BlockHash | Block, *, with_transactions: bool = False + ) -> None | BlockInfo: + if isinstance(block_id, BlockHash): + return await self._rpc.eth_get_block_by_hash( + block_id, with_transactions=with_transactions + ) + return await self._rpc.eth_get_block_by_number( + block_id, with_transactions=with_transactions + ) + + async def call( self, call: BaseBoundMethodCall, block: Block = BlockLabel.LATEST, @@ -406,23 +263,10 @@ async def eth_call( If ``sender_address`` is provided, it will be included in the call and affect the return value if the method uses ``msg.sender`` internally. """ - params = EthCallParams(to=call.contract_address, data=call.data_bytes, from_=sender_address) - - encoded_output = await rpc_call( - self._provider_session, - "eth_call", - bytes, - params, - block, - ) - return call.decode_output(encoded_output) - - async def _eth_send_raw_transaction(self, tx_bytes: bytes) -> TxHash: - """Sends a signed and serialized transaction.""" - return await rpc_call(self._provider_session, "eth_sendRawTransaction", TxHash, tx_bytes) - - async def _estimate_gas(self, params: EstimateGasParams, block: Block) -> int: - return await rpc_call(self._provider_session, "eth_estimateGas", int, params, block) + try: + return await self._rpc.eth_call(call, block=block, sender_address=sender_address) + except ProviderError as exc: + raise decode_contract_error(call.contract_abi, exc) from exc async def estimate_deploy( self, @@ -444,7 +288,7 @@ async def estimate_deploy( from_=sender_address, data=call.data_bytes, value=amount or Amount(0) ) try: - return await self._estimate_gas(params, block) + return await self._rpc.eth_estimate_gas(params, block) except ProviderError as exc: raise decode_contract_error(call.contract_abi, exc) from exc @@ -462,7 +306,7 @@ async def estimate_transfer( # source_address and amount are optional, # but if they are specified, we will fail here instead of later. params = EstimateGasParams(from_=source_address, to=destination_address, value=amount) - return await self._estimate_gas(params, block) + return await self._rpc.eth_estimate_gas(params, block) async def estimate_transact( self, @@ -488,52 +332,10 @@ async def estimate_transact( value=amount or Amount(0), ) try: - return await self._estimate_gas(params, block) + return await self._rpc.eth_estimate_gas(params, block) except ProviderError as exc: raise decode_contract_error(call.contract_abi, exc) from exc - async def eth_gas_price(self) -> Amount: - """Calls the ``eth_gasPrice`` RPC method.""" - return await rpc_call(self._provider_session, "eth_gasPrice", Amount) - - async def eth_block_number(self) -> int: - """Calls the ``eth_blockNumber`` RPC method.""" - return await rpc_call(self._provider_session, "eth_blockNumber", int) - - async def eth_get_block_by_hash( - self, block_hash: BlockHash, *, with_transactions: bool = False - ) -> None | BlockInfo: - """Calls the ``eth_getBlockByHash`` RPC method.""" - # Need an explicit cast, mypy doesn't work with union types correctly. - # See https://github.com/python/mypy/issues/16935 - return cast( - "None | BlockInfo", - await rpc_call( - self._provider_session, - "eth_getBlockByHash", - None | BlockInfo, # type: ignore[arg-type] - block_hash, - with_transactions, - ), - ) - - async def eth_get_block_by_number( - self, block: Block = BlockLabel.LATEST, *, with_transactions: bool = False - ) -> None | BlockInfo: - """Calls the ``eth_getBlockByNumber`` RPC method.""" - # Need an explicit cast, mypy doesn't work with union types correctly. - # See https://github.com/python/mypy/issues/16935 - return cast( - "None | BlockInfo", - await rpc_call( - self._provider_session, - "eth_getBlockByNumber", - None | BlockInfo, # type: ignore[arg-type] - block, - with_transactions, - ), - ) - async def broadcast_transfer( self, signer: Signer, @@ -546,13 +348,13 @@ async def broadcast_transfer( If ``gas`` is ``None``, the required amount of gas is estimated first, otherwise the provided value is used. """ - chain_id = await self.eth_chain_id() + chain_id = await self.chain_id() if gas is None: gas = await self.estimate_transfer(signer.address, destination_address, amount) # TODO (#19): implement gas strategies - max_gas_price = await self.eth_gas_price() + max_gas_price = await self._rpc.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) - nonce = await self.eth_get_transaction_count(signer.address, BlockLabel.PENDING) + nonce = await self._rpc.eth_get_transaction_count(signer.address, BlockLabel.PENDING) tx = cast( "TransactionDictType", unstructure( @@ -568,7 +370,7 @@ async def broadcast_transfer( ), ) signed_tx = signer.sign_transaction(tx) - return await self._eth_send_raw_transaction(signed_tx) + return await self._rpc.eth_send_raw_transaction(signed_tx) async def transfer( self, @@ -616,13 +418,13 @@ async def deploy( if not call.payable and amount.as_wei() != 0: raise ValueError("This constructor does not accept an associated payment") - chain_id = await self.eth_chain_id() + chain_id = await self.chain_id() if gas is None: gas = await self.estimate_deploy(signer.address, call, amount=amount) # TODO (#19): implement gas strategies - max_gas_price = await self.eth_gas_price() + max_gas_price = await self._rpc.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) - nonce = await self.eth_get_transaction_count(signer.address, BlockLabel.PENDING) + nonce = await self._rpc.eth_get_transaction_count(signer.address, BlockLabel.PENDING) tx = cast( "TransactionDictType", unstructure( @@ -638,7 +440,7 @@ async def deploy( ), ) signed_tx = signer.sign_transaction(tx) - tx_hash = await self._eth_send_raw_transaction(signed_tx) + tx_hash = await self._rpc.eth_send_raw_transaction(signed_tx) receipt = await self.wait_for_transaction_receipt(tx_hash) if not receipt.succeeded: @@ -672,15 +474,15 @@ async def broadcast_transact( if not call.payable and amount.as_wei() != 0: raise ValueError("This method does not accept an associated payment") - chain_id = await self.eth_chain_id() + chain_id = await self.chain_id() if gas is None: gas = await self.estimate_transact( signer.address, call, amount=amount, block=BlockLabel.PENDING ) # TODO (#19): implement gas strategies - max_gas_price = await self.eth_gas_price() + max_gas_price = await self._rpc.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) - nonce = await self.eth_get_transaction_count(signer.address, BlockLabel.PENDING) + nonce = await self._rpc.eth_get_transaction_count(signer.address, BlockLabel.PENDING) tx = cast( "TransactionDictType", unstructure( @@ -697,7 +499,7 @@ async def broadcast_transact( ), ) signed_tx = signer.sign_transaction(tx) - return await self._eth_send_raw_transaction(signed_tx) + return await self._rpc.eth_send_raw_transaction(signed_tx) async def transact( self, @@ -735,7 +537,7 @@ async def transact( results = {} for event in return_events: event_filter = event() - log_entries = await self.eth_get_logs( + log_entries = await self._rpc.eth_get_logs( source=event_filter.contract_address, event_filter=EventFilter(event_filter.topics), from_block=receipt.block_number, @@ -753,106 +555,11 @@ async def transact( return results - async def eth_get_logs( - self, - source: None | Address | Iterable[Address] = None, - event_filter: None | EventFilter = None, - from_block: Block = BlockLabel.LATEST, - to_block: Block = BlockLabel.LATEST, - ) -> tuple[LogEntry, ...]: - """Calls the ``eth_getLogs`` RPC method.""" - if isinstance(source, Iterable): - source = tuple(source) - params = FilterParams( - from_block=from_block, - to_block=to_block, - address=source, - topics=event_filter.topics if event_filter is not None else None, - ) - return await rpc_call(self._provider_session, "eth_getLogs", tuple[LogEntry, ...], params) - - async def eth_new_block_filter(self) -> BlockFilter: - """Calls the ``eth_newBlockFilter`` RPC method.""" - result, provider_path = await rpc_call_pin( - self._provider_session, "eth_newBlockFilter", int - ) - return BlockFilter(id=result, provider_path=provider_path) - - async def eth_new_pending_transaction_filter(self) -> PendingTransactionFilter: - """Calls the ``eth_newPendingTransactionFilter`` RPC method.""" - result, provider_path = await rpc_call_pin( - self._provider_session, "eth_newPendingTransactionFilter", int - ) - return PendingTransactionFilter(id=result, provider_path=provider_path) - - async def eth_new_filter( - self, - source: None | Address | Iterable[Address] = None, - event_filter: None | EventFilter = None, - from_block: Block = BlockLabel.LATEST, - to_block: Block = BlockLabel.LATEST, - ) -> LogFilter: - """Calls the ``eth_newFilter`` RPC method.""" - if isinstance(source, Iterable): - source = tuple(source) - params = FilterParams( - from_block=from_block, - to_block=to_block, - address=source, - topics=event_filter.topics if event_filter is not None else None, - ) - result, provider_path = await rpc_call_pin( - self._provider_session, "eth_newFilter", int, params - ) - return LogFilter(id=result, provider_path=provider_path) - - async def _query_filter( - self, method_name: str, filter_: BlockFilter | PendingTransactionFilter | LogFilter - ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: - if isinstance(filter_, BlockFilter): - return await rpc_call_at_pin( - self._provider_session, - filter_.provider_path, - method_name, - tuple[BlockHash, ...], - filter_.id, - ) - if isinstance(filter_, PendingTransactionFilter): - return await rpc_call_at_pin( - self._provider_session, - filter_.provider_path, - method_name, - tuple[TxHash, ...], - filter_.id, - ) - return await rpc_call_at_pin( - self._provider_session, - filter_.provider_path, - method_name, - tuple[LogEntry, ...], - filter_.id, - ) - - async def eth_get_filter_logs( - self, filter_: BlockFilter | PendingTransactionFilter | LogFilter - ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: - """Calls the ``eth_getFilterLogs`` RPC method.""" - return await self._query_filter("eth_getFilterLogs", filter_) - - async def eth_get_filter_changes( - self, filter_: BlockFilter | PendingTransactionFilter | LogFilter - ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: - """ - Calls the ``eth_getFilterChanges`` RPC method. - Depending on what ``filter_`` was, returns a tuple of corresponding results. - """ - return await self._query_filter("eth_getFilterChanges", filter_) - async def iter_blocks(self, poll_interval: int = 1) -> AsyncIterator[BlockHash]: """Yields hashes of new blocks being mined.""" - block_filter = await self.eth_new_block_filter() + block_filter = await self._rpc.eth_new_block_filter() while True: - block_hashes = await self.eth_get_filter_changes(block_filter) + block_hashes = await self._rpc.eth_get_filter_changes(block_filter) for block_hash in block_hashes: # We can't ensure it statically, since `eth_getFilterChanges` return type depends # on the filter passed to it. @@ -861,9 +568,9 @@ async def iter_blocks(self, poll_interval: int = 1) -> AsyncIterator[BlockHash]: async def iter_pending_transactions(self, poll_interval: int = 1) -> AsyncIterator[TxHash]: """Yields hashes of new transactions being submitted.""" - tx_filter = await self.eth_new_pending_transaction_filter() + tx_filter = await self._rpc.eth_new_pending_transaction_filter() while True: - tx_hashes = await self.eth_get_filter_changes(tx_filter) + tx_hashes = await self._rpc.eth_get_filter_changes(tx_filter) for tx_hash in tx_hashes: # We can't ensure it statically, since `eth_getFilterChanges` return type depends # on the filter passed to it. @@ -882,14 +589,14 @@ async def iter_events( The fields that were hashed when converted to topics (that is, fields of reference types) are set to ``None``. """ - log_filter = await self.eth_new_filter( + log_filter = await self._rpc.eth_new_filter( source=event_filter.contract_address, event_filter=EventFilter(event_filter.topics), from_block=from_block, to_block=to_block, ) while True: - log_entries = await self.eth_get_filter_changes(log_filter) + log_entries = await self._rpc.eth_get_filter_changes(log_filter) for log_entry in log_entries: # We can't ensure it statically, since `eth_getFilterChanges` return type depends # on the filter passed to it. diff --git a/pons/_client_rpc.py b/pons/_client_rpc.py new file mode 100644 index 0000000..9ede0f1 --- /dev/null +++ b/pons/_client_rpc.py @@ -0,0 +1,393 @@ +from collections.abc import Iterable, Iterator +from contextlib import contextmanager +from dataclasses import dataclass +from typing import Any, TypeVar, cast + +from compages import StructuringError +from ethereum_rpc import ( + Address, + Amount, + Block, + BlockHash, + BlockInfo, + BlockLabel, + EstimateGasParams, + EthCallParams, + FilterParams, + LogEntry, + RPCError, + RPCErrorCode, + TxHash, + TxInfo, + TxReceipt, + structure, + unstructure, +) + +from ._contract import BaseBoundMethodCall +from ._contract_abi import EventFilter +from ._provider import InvalidResponse, ProviderSession + + +@dataclass +class BlockFilter: + id: int + provider_path: tuple[int, ...] + + +@dataclass +class PendingTransactionFilter: + id: int + provider_path: tuple[int, ...] + + +@dataclass +class LogFilter: + id: int + provider_path: tuple[int, ...] + + +class BadResponseFormat(Exception): + """Raised if the RPC provider returned an unexpectedly formatted response.""" + + +class ProviderError(Exception): + """ + A general problem with fulfilling the request at the provider's side. + + This means the provider sent a correct response with an error code + and possibly some associated data. + """ + + raw_code: int + """The error code returned by the server.""" + + code: None | RPCErrorCode + """The parsed error code (if known).""" + + message: str + """The error message.""" + + data: None | bytes + """The associated data (if any).""" + + @classmethod + def from_rpc_error(cls, exc: RPCError) -> "ProviderError": + return cls(exc.code, exc.parsed_code, exc.message, exc.data) + + def __init__( + self, raw_code: int, code: None | RPCErrorCode, message: str, data: None | bytes = None + ): + super().__init__(raw_code, code, message, data) + self.raw_code = raw_code + self.code = code + self.message = message + self.data = data + + def __str__(self) -> str: + # Substitute the known code if any, or report the raw integer value otherwise + code = self.code or self.raw_code + return f"Provider error ({code}): {self.message}" + ( + f" (data: {self.data.hex()})" if self.data else "" + ) + + +@contextmanager +def convert_errors(method_name: str) -> Iterator[None]: + try: + yield + except (StructuringError, InvalidResponse) as exc: + raise BadResponseFormat(f"{method_name}: {exc}") from exc + except RPCError as exc: + raise ProviderError.from_rpc_error(exc) from exc + + +RetType = TypeVar("RetType") + + +async def rpc_call( + provider_session: ProviderSession, method_name: str, ret_type: type[RetType], *args: Any +) -> RetType: + """Catches various response formatting errors and returns them in a unified way.""" + with convert_errors(method_name): + result = await provider_session.rpc(method_name, *(unstructure(arg) for arg in args)) + return structure(ret_type, result) + + +async def rpc_call_pin( + provider_session: ProviderSession, method_name: str, ret_type: type[RetType], *args: Any +) -> tuple[RetType, tuple[int, ...]]: + """Catches various response formatting errors and returns them in a unified way.""" + with convert_errors(method_name): + result, provider_path = await provider_session.rpc_and_pin( + method_name, *(unstructure(arg) for arg in args) + ) + return structure(ret_type, result), provider_path + + +async def rpc_call_at_pin( + provider_session: ProviderSession, + provider_path: tuple[int, ...], + method_name: str, + ret_type: type[RetType], + *args: Any, +) -> RetType: + """Catches various response formatting errors and returns them in a unified way.""" + with convert_errors(method_name): + result = await provider_session.rpc_at_pin( + provider_path, method_name, *(unstructure(arg) for arg in args) + ) + return structure(ret_type, result) + + +class ClientSessionRPC: + """ + The hub for methods which directly correspond to Ethereum RPC calls. + + The methods of this class may raise the following exceptions: + :py:class:`ProviderError`, + :py:class:`Unreachable`, + :py:class:`BadResponseFormat`, + a provider-specific derived class of :py:class:`ProtocolError`. + """ + + def __init__(self, provider_session: ProviderSession): + self._provider_session = provider_session + + async def net_version(self) -> str: + """Calls the ``net_version`` RPC method.""" + return await rpc_call(self._provider_session, "net_version", str) + + async def eth_chain_id(self) -> int: + """Calls the ``eth_chainId`` RPC method.""" + return await rpc_call(self._provider_session, "eth_chainId", int) + + async def eth_get_balance(self, address: Address, block: Block = BlockLabel.LATEST) -> Amount: + """Calls the ``eth_getBalance`` RPC method.""" + return await rpc_call(self._provider_session, "eth_getBalance", Amount, address, block) + + async def eth_get_transaction_by_hash(self, tx_hash: TxHash) -> None | TxInfo: + """Calls the ``eth_getTransactionByHash`` RPC method.""" + # Need an explicit cast, mypy doesn't work with union types correctly. + # See https://github.com/python/mypy/issues/16935 + return cast( + "None | TxInfo", + await rpc_call( + self._provider_session, + "eth_getTransactionByHash", + None | TxInfo, # type: ignore[arg-type] + tx_hash, + ), + ) + + async def eth_get_transaction_receipt(self, tx_hash: TxHash) -> None | TxReceipt: + """Calls the ``eth_getTransactionReceipt`` RPC method.""" + # Need an explicit cast, mypy doesn't work with union types correctly. + # See https://github.com/python/mypy/issues/16935 + return cast( + "None | TxReceipt", + await rpc_call( + self._provider_session, + "eth_getTransactionReceipt", + None | TxReceipt, # type: ignore[arg-type] + tx_hash, + ), + ) + + async def eth_get_transaction_count( + self, address: Address, block: Block = BlockLabel.LATEST + ) -> int: + """Calls the ``eth_getTransactionCount`` RPC method.""" + return await rpc_call( + self._provider_session, + "eth_getTransactionCount", + int, + address, + block, + ) + + async def eth_get_code(self, address: Address, block: Block = BlockLabel.LATEST) -> bytes: + """Calls the ``eth_getCode`` RPC method.""" + return await rpc_call(self._provider_session, "eth_getCode", bytes, address, block) + + async def eth_get_storage_at( + self, address: Address, position: int, block: Block = BlockLabel.LATEST + ) -> bytes: + """Calls the ``eth_getCode`` RPC method.""" + return await rpc_call( + self._provider_session, + "eth_getStorageAt", + bytes, + address, + position, + block, + ) + + async def eth_call( + self, + call: BaseBoundMethodCall, + block: Block = BlockLabel.LATEST, + sender_address: None | Address = None, + ) -> Any: + """ + Sends a prepared contact method call to the provided address. + Returns the decoded output. + + If ``sender_address`` is provided, it will be included in the call + and affect the return value if the method uses ``msg.sender`` internally. + """ + params = EthCallParams(to=call.contract_address, data=call.data_bytes, from_=sender_address) + + encoded_output = await rpc_call( + self._provider_session, + "eth_call", + bytes, + params, + block, + ) + return call.decode_output(encoded_output) + + async def eth_send_raw_transaction(self, tx_bytes: bytes) -> TxHash: + """Sends a signed and serialized transaction.""" + return await rpc_call(self._provider_session, "eth_sendRawTransaction", TxHash, tx_bytes) + + async def eth_estimate_gas(self, params: EstimateGasParams, block: Block) -> int: + """Calls the ``eth_estimateGas`` RPC method.""" + return await rpc_call(self._provider_session, "eth_estimateGas", int, params, block) + + async def eth_gas_price(self) -> Amount: + """Calls the ``eth_gasPrice`` RPC method.""" + return await rpc_call(self._provider_session, "eth_gasPrice", Amount) + + async def eth_block_number(self) -> int: + """Calls the ``eth_blockNumber`` RPC method.""" + return await rpc_call(self._provider_session, "eth_blockNumber", int) + + async def eth_get_block_by_hash( + self, block_hash: BlockHash, *, with_transactions: bool = False + ) -> None | BlockInfo: + """Calls the ``eth_getBlockByHash`` RPC method.""" + # Need an explicit cast, mypy doesn't work with union types correctly. + # See https://github.com/python/mypy/issues/16935 + return cast( + "None | BlockInfo", + await rpc_call( + self._provider_session, + "eth_getBlockByHash", + None | BlockInfo, # type: ignore[arg-type] + block_hash, + with_transactions, + ), + ) + + async def eth_get_block_by_number( + self, block: Block = BlockLabel.LATEST, *, with_transactions: bool = False + ) -> None | BlockInfo: + """Calls the ``eth_getBlockByNumber`` RPC method.""" + # Need an explicit cast, mypy doesn't work with union types correctly. + # See https://github.com/python/mypy/issues/16935 + return cast( + "None | BlockInfo", + await rpc_call( + self._provider_session, + "eth_getBlockByNumber", + None | BlockInfo, # type: ignore[arg-type] + block, + with_transactions, + ), + ) + + async def eth_get_logs( + self, + source: None | Address | Iterable[Address] = None, + event_filter: None | EventFilter = None, + from_block: Block = BlockLabel.LATEST, + to_block: Block = BlockLabel.LATEST, + ) -> tuple[LogEntry, ...]: + """Calls the ``eth_getLogs`` RPC method.""" + if isinstance(source, Iterable): + source = tuple(source) + params = FilterParams( + from_block=from_block, + to_block=to_block, + address=source, + topics=event_filter.topics if event_filter is not None else None, + ) + return await rpc_call(self._provider_session, "eth_getLogs", tuple[LogEntry, ...], params) + + async def eth_new_block_filter(self) -> BlockFilter: + """Calls the ``eth_newBlockFilter`` RPC method.""" + result, provider_path = await rpc_call_pin( + self._provider_session, "eth_newBlockFilter", int + ) + return BlockFilter(id=result, provider_path=provider_path) + + async def eth_new_pending_transaction_filter(self) -> PendingTransactionFilter: + """Calls the ``eth_newPendingTransactionFilter`` RPC method.""" + result, provider_path = await rpc_call_pin( + self._provider_session, "eth_newPendingTransactionFilter", int + ) + return PendingTransactionFilter(id=result, provider_path=provider_path) + + async def eth_new_filter( + self, + source: None | Address | Iterable[Address] = None, + event_filter: None | EventFilter = None, + from_block: Block = BlockLabel.LATEST, + to_block: Block = BlockLabel.LATEST, + ) -> LogFilter: + """Calls the ``eth_newFilter`` RPC method.""" + if isinstance(source, Iterable): + source = tuple(source) + params = FilterParams( + from_block=from_block, + to_block=to_block, + address=source, + topics=event_filter.topics if event_filter is not None else None, + ) + result, provider_path = await rpc_call_pin( + self._provider_session, "eth_newFilter", int, params + ) + return LogFilter(id=result, provider_path=provider_path) + + async def _query_filter( + self, method_name: str, filter_: BlockFilter | PendingTransactionFilter | LogFilter + ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: + if isinstance(filter_, BlockFilter): + return await rpc_call_at_pin( + self._provider_session, + filter_.provider_path, + method_name, + tuple[BlockHash, ...], + filter_.id, + ) + if isinstance(filter_, PendingTransactionFilter): + return await rpc_call_at_pin( + self._provider_session, + filter_.provider_path, + method_name, + tuple[TxHash, ...], + filter_.id, + ) + return await rpc_call_at_pin( + self._provider_session, + filter_.provider_path, + method_name, + tuple[LogEntry, ...], + filter_.id, + ) + + async def eth_get_filter_logs( + self, filter_: BlockFilter | PendingTransactionFilter | LogFilter + ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: + """Calls the ``eth_getFilterLogs`` RPC method.""" + return await self._query_filter("eth_getFilterLogs", filter_) + + async def eth_get_filter_changes( + self, filter_: BlockFilter | PendingTransactionFilter | LogFilter + ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: + """ + Calls the ``eth_getFilterChanges`` RPC method. + Depending on what ``filter_`` was, returns a tuple of corresponding results. + """ + return await self._query_filter("eth_getFilterChanges", filter_) diff --git a/pons/_provider.py b/pons/_provider.py index a83d00d..10bbee4 100644 --- a/pons/_provider.py +++ b/pons/_provider.py @@ -21,11 +21,27 @@ class Unreachable(Exception): """Raised when there is a problem connecting to the provider.""" -class ProtocolError(Exception): - """A protocol-specific error.""" +class ProtocolError(ABC, Exception): + """ + A protocol-specific error, indicating that the provider returned an error status + with no additional information allowing to categorize the error further. + + See the provider-specifc derived class for this exception for more details. + """ class HTTPError(ProtocolError): + """ + Raised when the provider returns a response with a status code other than 200, + and no ``"error"`` field in the associated JSON data. + """ + + status: HTTPStatus + """The HTTP status of the response.""" + + message: str + """The response body.""" + def __init__(self, status_code: int, message: str): try: status = HTTPStatus(status_code) diff --git a/tests/test_client.py b/tests/test_client.py index 235c8e3..e40f953 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,4 +1,3 @@ -import os import re from contextlib import contextmanager from pathlib import Path @@ -6,15 +5,12 @@ import pytest import trio from ethereum_rpc import ( - Address, Amount, BlockHash, - BlockLabel, RPCError, RPCErrorCode, TxHash, TxInfo, - keccak, ) from pons import ( @@ -25,7 +21,6 @@ ContractLegacyError, ContractPanic, DeployedContract, - Either, LocalProvider, Method, Mutability, @@ -51,15 +46,6 @@ def monkeypatched(obj, attr, patch): setattr(obj, attr, original_value) -def normalize_topics(topics): - """ - 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 - (where even single elements are 1-tuples). - """ - return tuple((elem,) for elem in topics) - - async def test_net_version(local_provider, session): net_version1 = await session.net_version() assert net_version1 == "1" @@ -81,7 +67,7 @@ async def test_net_version_type_check(local_provider, session): await session.net_version() -async def test_eth_chain_id(): +async def test_chain_id(): local_provider = LocalProvider(root_balance=Amount.ether(100), chain_id=123) client = Client(local_provider) @@ -90,53 +76,34 @@ def mock_rpc(_method, *_args): raise NotImplementedError # pragma: no cover async with client.session() as session: - chain_id1 = await session.eth_chain_id() + chain_id1 = await session.chain_id() assert chain_id1 == 123 # The result should have been cached the first time with monkeypatched(local_provider, "rpc", mock_rpc): - chain_id2 = await session.eth_chain_id() + chain_id2 = await session.chain_id() assert chain_id1 == chain_id2 -async def test_eth_get_balance(session, root_signer, another_signer): +async def test_get_block(session, root_signer, another_signer): to_transfer = Amount.ether(10) await session.transfer(root_signer, another_signer.address, to_transfer) - acc1_balance = await session.eth_get_balance(another_signer.address) - assert acc1_balance == to_transfer - - # Non-existent address (which is technically just an unfunded address) - random_addr = Address(os.urandom(20)) - balance = await session.eth_get_balance(random_addr) - assert balance == Amount.ether(0) + block_info = await session.get_block(1, with_transactions=True) + assert all(isinstance(tx, TxInfo) for tx in block_info.transactions) -async def test_eth_get_transaction_receipt(local_provider, session, root_signer, another_signer): - local_provider.disable_auto_mine_transactions() - tx_hash = await session.broadcast_transfer( - root_signer, another_signer.address, Amount.ether(10) - ) - receipt = await session.eth_get_transaction_receipt(tx_hash) - assert receipt is None - - local_provider.enable_auto_mine_transactions() - receipt = await session.eth_get_transaction_receipt(tx_hash) - assert receipt.succeeded - - # A non-existent transaction - receipt = await session.eth_get_transaction_receipt(TxHash(os.urandom(32))) - assert receipt is None - + block_info2 = await session.get_block(block_info.hash_, with_transactions=True) + assert block_info2 == block_info -async def test_eth_get_transaction_count(local_provider, session, root_signer, another_signer): - assert await session.eth_get_transaction_count(root_signer.address) == 0 - await session.transfer(root_signer, another_signer.address, Amount.ether(10)) - assert await session.eth_get_transaction_count(root_signer.address) == 1 + # no transactions + block_info = await session.get_block(1) + assert all(isinstance(tx, TxHash) for tx in block_info.transactions) - # Check that pending transactions are accounted for - local_provider.disable_auto_mine_transactions() - await session.broadcast_transfer(root_signer, another_signer.address, Amount.ether(10)) - assert await session.eth_get_transaction_count(root_signer.address, BlockLabel.PENDING) == 2 + # non-existent block + block_info = await session.get_block(100, with_transactions=True) + assert block_info is None + block_info = await session.get_block(BlockHash(b"\x00" * 32), with_transactions=True) + assert block_info is None async def test_wait_for_transaction_receipt( @@ -177,45 +144,20 @@ async def delayed_enable_mining(): assert receipt.succeeded -async def test_eth_call(session, compiled_contracts, root_signer, another_signer): - compiled_contract = compiled_contracts["BasicContract"] - deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123)) - result = await session.eth_call(deployed_contract.method.getState(456)) - assert result == (123 + 456,) - - # With a real provider, if `sender_address` is not given, it will default to the zero address. - result = await session.eth_call(deployed_contract.method.getSender()) - assert result == (Address(b"\x00" * 20),) - - # Seems to be another tester chain limitation: even though `eth_call` does not spend gas, - # the `sender_address` still needs to be funded. - await session.transfer(root_signer, another_signer.address, Amount.ether(10)) - - result = await session.eth_call( - deployed_contract.method.getSender(), sender_address=another_signer.address - ) - assert result == (another_signer.address,) - - -async def test_eth_call_pending(local_provider, session, compiled_contracts, root_signer): - compiled_contract = compiled_contracts["BasicContract"] +async def test_call_contract_error(session, compiled_contracts, root_signer): + """Tests that `call()` correctly decodes a contract error.""" + compiled_contract = compiled_contracts["TestErrors"] deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123)) - local_provider.disable_auto_mine_transactions() - await session.broadcast_transact(root_signer, deployed_contract.method.setState(456)) - - # This uses the state of the last finalized block - result = await session.eth_call(deployed_contract.method.getState(0)) - assert result == (123,) - - # This also uses the state change introduced by the pending transaction - result = await session.eth_call(deployed_contract.method.getState(0), block=BlockLabel.PENDING) - assert result == (456,) + with pytest.raises(ContractError) as exc: + await session.call(deployed_contract.method.viewError(4)) + assert exc.value.error == deployed_contract.error.CustomError + assert exc.value.data == {"x": 4} -async def test_eth_call_decoding_error(session, compiled_contracts, root_signer): +async def test_call_decoding_error(session, compiled_contracts, root_signer): """ - Tests that `eth_call()` propagates an error on mismatch of the declared output signature + 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). """ compiled_contract = compiled_contracts["BasicContract"] @@ -240,7 +182,7 @@ async def test_eth_call_decoding_error(session, compiled_contracts, root_signer) ) with pytest.raises(ABIDecodingError, match=expected_message): - await session.eth_call(wrong_contract.method.getState(456)) + await session.call(wrong_contract.method.getState(456)) async def test_estimate_deploy(session, compiled_contracts, root_signer): @@ -276,41 +218,26 @@ async def test_estimate_transact(session, compiled_contracts, root_signer): assert gas > 0 -async def test_eth_gas_price(session): - gas_price = await session.eth_gas_price() - assert isinstance(gas_price, Amount) - - -async def test_eth_block_number(session, root_signer, another_signer): - 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.eth_block_number() - - block_info = await session.eth_get_block_by_number(block_num - 1, with_transactions=True) - assert block_info.transactions[0].value == Amount.ether(2) - - async def test_transfer(session, root_signer, another_signer): # Regular transfer - root_balance = await session.eth_get_balance(root_signer.address) + root_balance = await session.get_balance(root_signer.address) to_transfer = Amount.ether(10) await session.transfer(root_signer, another_signer.address, to_transfer) - root_balance_after = await session.eth_get_balance(root_signer.address) - acc1_balance_after = await session.eth_get_balance(another_signer.address) + root_balance_after = await session.get_balance(root_signer.address) + acc1_balance_after = await session.get_balance(another_signer.address) assert acc1_balance_after == to_transfer assert root_balance - root_balance_after > to_transfer async def test_transfer_custom_gas(session, root_signer, another_signer): - root_balance = await session.eth_get_balance(root_signer.address) + root_balance = await session.get_balance(root_signer.address) to_transfer = Amount.ether(10) # Override gas estimate # The standard transfer gas cost is 21000, we're being cautious here. await session.transfer(root_signer, another_signer.address, to_transfer, gas=22000) - root_balance_after = await session.eth_get_balance(root_signer.address) - acc1_balance_after = await session.eth_get_balance(another_signer.address) + root_balance_after = await session.get_balance(root_signer.address) + acc1_balance_after = await session.get_balance(another_signer.address) assert acc1_balance_after == to_transfer assert root_balance - root_balance_after > to_transfer @@ -345,7 +272,7 @@ async def test_deploy(local_provider, session, compiled_contracts, root_signer): # Normal deploy deployed_contract = await session.deploy(root_signer, basic_contract.constructor(123)) - result = await session.eth_call(deployed_contract.method.getState(456)) + result = await session.call(deployed_contract.method.getState(456)) assert result == (123 + 456,) with pytest.raises(ValueError, match="This constructor does not accept an associated payment"): @@ -358,7 +285,7 @@ async def test_deploy(local_provider, session, compiled_contracts, root_signer): contract = await session.deploy( root_signer, payable_constructor.constructor(1), Amount.ether(1) ) - balance = await session.eth_get_balance(contract.address) + balance = await session.get_balance(contract.address) assert balance == Amount.ether(1) # When gas is set manually, the gas estimation step is skipped, @@ -392,7 +319,7 @@ async def test_transact(session, compiled_contracts, root_signer): # Normal transact deployed_contract = await session.deploy(root_signer, basic_contract.constructor(123)) await session.transact(root_signer, deployed_contract.method.setState(456)) - result = await session.eth_call(deployed_contract.method.getState(789)) + result = await session.call(deployed_contract.method.getState(789)) assert result == (456 + 789,) with pytest.raises( @@ -410,7 +337,7 @@ async def test_transact(session, compiled_contracts, root_signer): await session.transact( root_signer, deployed_contract.method.payableSetState(456), Amount.ether(1) ) - balance = await session.eth_get_balance(deployed_contract.address) + balance = await session.get_balance(deployed_contract.address) assert balance == Amount.ether(1) # Not enough gas @@ -496,389 +423,6 @@ async def delayed_enable_mining(): assert results[x2] == results_for(x2) -async def test_get_block(session, root_signer, another_signer): - to_transfer = Amount.ether(10) - await session.transfer(root_signer, another_signer.address, to_transfer) - - block_info = await session.eth_get_block_by_number(1, with_transactions=True) - assert all(isinstance(tx, TxInfo) for tx in block_info.transactions) - - block_info2 = await session.eth_get_block_by_hash(block_info.hash_, with_transactions=True) - assert block_info2 == block_info - - # no transactions - block_info = await session.eth_get_block_by_number(1) - assert all(isinstance(tx, TxHash) for tx in block_info.transactions) - - # non-existent block - block_info = await session.eth_get_block_by_number(100, with_transactions=True) - assert block_info is None - block_info = await session.eth_get_block_by_hash( - BlockHash(b"\x00" * 32), with_transactions=True - ) - assert block_info is None - - -async def test_get_block_pending(local_provider, session, root_signer, another_signer): - await session.transfer(root_signer, another_signer.address, Amount.ether(1)) - - local_provider.disable_auto_mine_transactions() - tx_hash = await session.broadcast_transfer( - root_signer, another_signer.address, Amount.ether(10) - ) - - block_info = await session.eth_get_block_by_number(BlockLabel.PENDING, with_transactions=True) - 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 block_info.transactions[0].hash_ == tx_hash - assert block_info.transactions[0].value == Amount.ether(10) - - block_info = await session.eth_get_block_by_number(BlockLabel.PENDING, with_transactions=False) - 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): - to_transfer = Amount.ether(1) - - tx_hash = await session.broadcast_transfer(root_signer, another_signer.address, to_transfer) - tx_info = await session.eth_get_transaction_by_hash(tx_hash) - assert tx_info.value == to_transfer - - non_existent = TxHash(b"abcd" * 8) - tx_info = await session.eth_get_transaction_by_hash(non_existent) - assert tx_info is None - - local_provider.disable_auto_mine_transactions() - tx_hash = await session.broadcast_transfer(root_signer, another_signer.address, to_transfer) - tx_info = await session.eth_get_transaction_by_hash(tx_hash) - - 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): - compiled_contract = compiled_contracts["EmptyContract"] - deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123)) - bytecode = await session.eth_get_code(deployed_contract.address, block=BlockLabel.LATEST) - - # The bytecode being deployed is not the code that will be stored on chain, - # but some code that, having been executed, returns the code that will be stored on chain. - # So all we can do is check that the code stored on chain is a part of the initialization code. - assert bytecode in compiled_contract.bytecode - - -async def test_eth_get_storage_at(session, root_signer, compiled_contracts): - x = 0xAB - y_key = Address(os.urandom(20)) - y_val = 0xCD - - compiled_contract = compiled_contracts["Storage"] - deployed_contract = await session.deploy( - root_signer, compiled_contract.constructor(x, y_key, y_val) - ) - - # Get the regular stored value - storage = await session.eth_get_storage_at( - deployed_contract.address, 0, block=BlockLabel.LATEST - ) - assert storage == b"\x00" * 31 + x.to_bytes(1, byteorder="big") - - # Get the value of the mapping - position = int.from_bytes( - keccak( - # left-padded key - b"\x00" * 12 - + bytes(y_key) - # left-padded position of the mapping (1) - + b"\x00" * 31 - + b"\x01" - ), - byteorder="big", - ) - storage = await session.eth_get_storage_at( - deployed_contract.address, position, block=BlockLabel.LATEST - ) - assert storage == b"\x00" * 31 + y_val.to_bytes(1, byteorder="big") - - -async def test_eth_get_filter_changes_bad_response(local_provider, session, monkeypatch): - block_filter = await session.eth_new_block_filter() - - def mock_rpc(_method, *_args): - return {"foo": 1} - - monkeypatch.setattr(local_provider, "rpc", mock_rpc) - - with pytest.raises( - BadResponseFormat, - match=r"eth_getFilterChanges: Can only structure a tuple or a list into a tuple generic", - ): - await session.eth_get_filter_changes(block_filter) - - -async def test_block_filter(session, root_signer, another_signer): - to_transfer = Amount.ether(1) - - await session.transfer(root_signer, another_signer.address, to_transfer) - - block_filter = await session.eth_new_block_filter() - - await session.transfer(root_signer, another_signer.address, to_transfer) - await session.transfer(root_signer, another_signer.address, to_transfer) - - last_block = await session.eth_get_block_by_number(BlockLabel.LATEST) - prev_block = await session.eth_get_block_by_number(last_block.number - 1) - - block_hashes = await session.eth_get_filter_changes(block_filter) - assert block_hashes == (prev_block.hash_, last_block.hash_) - - await session.transfer(root_signer, another_signer.address, to_transfer) - - block_hashes = await session.eth_get_filter_changes(block_filter) - last_block = await session.eth_get_block_by_number(BlockLabel.LATEST) - assert block_hashes == (last_block.hash_,) - - block_hashes = await session.eth_get_filter_changes(block_filter) - assert len(block_hashes) == 0 - - -async def test_pending_transaction_filter(local_provider, session, root_signer, another_signer): - transaction_filter = await session.eth_new_pending_transaction_filter() - - to_transfer = Amount.ether(1) - - local_provider.disable_auto_mine_transactions() - tx_hash = await session.broadcast_transfer(root_signer, another_signer.address, to_transfer) - tx_hashes = await session.eth_get_filter_changes(transaction_filter) - assert tx_hashes == (tx_hash,) - - -async def test_eth_get_logs( - monkeypatch, local_provider, session, compiled_contracts, root_signer, another_signer -): - 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)) - contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) - await session.transact(root_signer, contract1.method.deposit(b"1234")) - await session.transact(another_signer, contract2.method.deposit2(b"4567")) - - entries = await session.eth_get_logs(source=contract2.address) - assert len(entries) == 1 - assert entries[0].address == contract2.address - assert ( - normalize_topics(entries[0].topics) - == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics - ) - - entries = await session.eth_get_logs( - source=[contract1.address, contract2.address], from_block=0 - ) - assert len(entries) == 2 - assert entries[0].address == contract1.address - assert entries[1].address == contract2.address - assert ( - normalize_topics(entries[0].topics) - == contract1.abi.event.Deposit(root_signer.address, b"1234").topics - ) - assert ( - normalize_topics(entries[1].topics) - == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics - ) - - # Test an invalid response - - def mock_rpc(_method, *_args): - return {"foo": 1} - - monkeypatch.setattr(local_provider, "rpc", mock_rpc) - - with pytest.raises( - BadResponseFormat, - match=r"eth_getLogs: Can only structure a tuple or a list into a tuple generic", - ): - await session.eth_get_logs(source=contract2.address) - - -async def test_eth_get_filter_logs(session, compiled_contracts, root_signer, another_signer): - 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)) - contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) - - log_filter = await session.eth_new_filter() - await session.transact(root_signer, contract1.method.deposit(b"1234")) - await session.transact(another_signer, contract2.method.deposit2(b"4567")) - - entries = await session.eth_get_filter_logs(log_filter) - assert len(entries) == 2 - assert entries[0].address == contract1.address - assert entries[1].address == contract2.address - - assert ( - normalize_topics(entries[0].topics) - == contract1.abi.event.Deposit(root_signer.address, b"1234").topics - ) - assert ( - normalize_topics(entries[1].topics) - == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics - ) - - -async def test_log_filter_all(session, compiled_contracts, root_signer, another_signer): - 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)) - contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) - - log_filter = await session.eth_new_filter() - await session.transact(root_signer, contract1.method.deposit(b"1234")) - await session.transact(another_signer, contract2.method.deposit2(b"4567")) - - entries = await session.eth_get_filter_changes(log_filter) - assert len(entries) == 2 - assert entries[0].address == contract1.address - assert entries[1].address == contract2.address - - assert ( - normalize_topics(entries[0].topics) - == contract1.abi.event.Deposit(root_signer.address, b"1234").topics - ) - assert ( - normalize_topics(entries[1].topics) - == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics - ) - - -async def test_log_filter_by_address(session, compiled_contracts, root_signer, another_signer): - 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)) - contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) - - # Filter by a single address - - log_filter = await session.eth_new_filter(source=contract2.address) - await session.transact(root_signer, contract1.method.deposit(b"1234")) - await session.transact(root_signer, contract2.method.deposit2(b"4567")) - - entries = await session.eth_get_filter_changes(log_filter) - assert len(entries) == 1 - assert entries[0].address == contract2.address - assert ( - normalize_topics(entries[0].topics) - == contract2.abi.event.Deposit2(root_signer.address, b"4567").topics - ) - - # Filter by several addresses - - contract3 = await session.deploy(another_signer, basic_contract.constructor(123)) - - log_filter = await session.eth_new_filter(source=[contract1.address, contract3.address]) - await session.transact(root_signer, contract1.method.deposit(b"1111")) - await session.transact(root_signer, contract2.method.deposit(b"2222")) - await session.transact(root_signer, contract3.method.deposit(b"3333")) - - entries = await session.eth_get_filter_changes(log_filter) - assert len(entries) == 2 - assert entries[0].address == contract1.address - assert ( - normalize_topics(entries[0].topics) - == contract1.abi.event.Deposit(root_signer.address, b"1111").topics - ) - assert entries[1].address == contract3.address - assert ( - normalize_topics(entries[1].topics) - == contract3.abi.event.Deposit(root_signer.address, b"3333").topics - ) - - -async def test_log_filter_by_topic(session, compiled_contracts, root_signer, another_signer): - 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)) - contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) - - # Filter by a specific topic or None at each position - - log_filter = await session.eth_new_filter(event_filter=contract2.abi.event.Deposit2(id=b"4567")) - # filtered out, wrong event type - await session.transact(root_signer, contract1.method.deposit(b"4567")) - # matches the filter - await session.transact(root_signer, contract1.method.deposit2(b"4567")) - # filtered out, wrong value - await session.transact(another_signer, contract2.method.deposit2(b"7890")) - # matches the filter - await session.transact(another_signer, contract2.method.deposit2(b"4567")) - - entries = await session.eth_get_filter_changes(log_filter) - assert len(entries) == 2 - assert entries[0].address == contract1.address - assert ( - normalize_topics(entries[0].topics) - == contract1.abi.event.Deposit2(root_signer.address, b"4567").topics - ) - assert entries[1].address == contract2.address - assert ( - normalize_topics(entries[1].topics) - == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics - ) - - # Filter by a list of topics - - event_filter = contract1.abi.event.Deposit(id=Either(b"1111", b"3333")) - log_filter = await session.eth_new_filter(event_filter=event_filter) - await session.transact(root_signer, contract1.method.deposit(b"1111")) - await session.transact(root_signer, contract1.method.deposit2(b"1111")) # filtered out - await session.transact(root_signer, contract1.method.deposit(b"2222")) # filtered out - await session.transact(another_signer, contract2.method.deposit(b"3333")) - - entries = await session.eth_get_filter_changes(log_filter) - assert len(entries) == 2 - assert entries[0].address == contract1.address - assert ( - normalize_topics(entries[0].topics) - == contract1.abi.event.Deposit(root_signer.address, b"1111").topics - ) - assert entries[1].address == contract2.address - assert ( - normalize_topics(entries[1].topics) - == contract2.abi.event.Deposit(another_signer.address, b"3333").topics - ) - - -async def test_log_filter_by_block_num(session, compiled_contracts, root_signer, another_signer): - 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)) - - await session.transact(root_signer, contract1.method.deposit(b"1111")) - block_num = await session.eth_block_number() - log_filter = await session.eth_new_filter(from_block=block_num + 1, to_block=block_num + 3) - - await session.transact(root_signer, contract1.method.deposit(b"2222")) # filter will start here - await session.transact(root_signer, contract1.method.deposit(b"3333")) - await session.transact(root_signer, contract1.method.deposit(b"4444")) # filter will stop here - await session.transact(root_signer, contract1.method.deposit(b"5555")) - - entries = await session.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_block_filter_high_level( autojump_clock, # noqa: ARG001 session, @@ -907,7 +451,7 @@ async def observer(): await session.transfer(root_signer, another_signer.address, Amount.ether(5)) for i, block_hash in enumerate(block_hashes): - block_info = await session.eth_get_block_by_hash(block_hash, with_transactions=True) + block_info = await session.get_block(block_hash, with_transactions=True) assert block_info.transactions[0].value == Amount.ether(i + 2) @@ -941,7 +485,7 @@ async def observer(): await session.transfer(root_signer, another_signer.address, Amount.ether(5)) for i, tx_hash in enumerate(tx_hashes): - tx_info = await session.eth_get_transaction_by_hash(tx_hash) + tx_info = await session.get_transaction(tx_hash) assert tx_info.value == Amount.ether(i + 2) @@ -993,96 +537,6 @@ async def observer(): assert events[2] == {"from_": another_signer.address, "id": b"1111", "value": 3, "value2": 4} -async def test_unknown_rpc_status_code(local_provider, session, monkeypatch): - def mock_rpc(_method, *_args): - # 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") - - monkeypatch.setattr(local_provider, "rpc", mock_rpc) - - with pytest.raises(ProviderError, match=r"Provider error \(666\): this method is possessed"): - await session.net_version() - - -async def check_rpc_error(awaitable, expected_code, expected_message, expected_data): - with pytest.raises(ProviderError) as exc: - await awaitable - - assert exc.value.code == expected_code - assert exc.value.message == expected_message - assert exc.value.data == expected_data - - -async def test_contract_exceptions(session, root_signer, compiled_contracts): - compiled_contract = compiled_contracts["TestErrors"] - contract = await session.deploy(root_signer, compiled_contract.constructor(999)) - - error_selector = keccak(b"Error(string)")[:4] - custom_error_selector = keccak(b"CustomError(uint256)")[:4] - - # `require(condition)` - kwargs = dict( - expected_code=RPCErrorCode.SERVER_ERROR, - expected_message="execution reverted", - expected_data=None, - ) - await check_rpc_error(session.eth_call(contract.method.viewError(0)), **kwargs) - - # `require(condition, message)` - kwargs = dict( - expected_code=RPCErrorCode.EXECUTION_ERROR, - expected_message="execution reverted", - expected_data=error_selector + encode_args((abi.string, "require(string)")), - ) - await check_rpc_error(session.eth_call(contract.method.viewError(1)), **kwargs) - - # `revert()` - kwargs = dict( - expected_code=RPCErrorCode.SERVER_ERROR, - expected_message="execution reverted", - expected_data=None, - ) - await check_rpc_error(session.eth_call(contract.method.viewError(2)), **kwargs) - - # `revert(message)` - kwargs = dict( - expected_code=RPCErrorCode.EXECUTION_ERROR, - expected_message="execution reverted", - expected_data=error_selector + encode_args((abi.string, "revert(string)")), - ) - await check_rpc_error(session.eth_call(contract.method.viewError(3)), **kwargs) - - # `revert CustomError(...)` - kwargs = dict( - 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.eth_call(contract.method.viewError(4)), **kwargs) - - -async def test_contract_panics(session, root_signer, compiled_contracts): - compiled_contract = compiled_contracts["TestErrors"] - contract = await session.deploy(root_signer, compiled_contract.constructor(999)) - - panic_selector = keccak(b"Panic(uint256)")[:4] - - await check_rpc_error( - session.eth_call(contract.method.viewPanic(0)), - expected_code=RPCErrorCode.EXECUTION_ERROR, - expected_message="execution reverted", - expected_data=panic_selector + encode_args((abi.uint(256), 0x01)), - ) - - await check_rpc_error( - session.eth_call(contract.method.viewPanic(1)), - expected_code=RPCErrorCode.EXECUTION_ERROR, - expected_message="execution reverted", - expected_data=panic_selector + encode_args((abi.uint(256), 0x11)), - ) - - async def test_contract_exceptions_high_level(session, root_signer, compiled_contracts): compiled_contract = compiled_contracts["TestErrors"] contract = await session.deploy(root_signer, compiled_contract.constructor(999)) diff --git a/tests/test_client_rpc.py b/tests/test_client_rpc.py new file mode 100644 index 0000000..1a671c3 --- /dev/null +++ b/tests/test_client_rpc.py @@ -0,0 +1,613 @@ +import os +from contextlib import contextmanager +from pathlib import Path + +import pytest +from ethereum_rpc import ( + Address, + Amount, + BlockHash, + BlockLabel, + RPCError, + RPCErrorCode, + TxHash, + TxInfo, + keccak, +) + +from pons import Either, abi, compile_contract_file +from pons._abi_types import encode_args +from pons._client_rpc import BadResponseFormat, ProviderError + + +@pytest.fixture +def compiled_contracts(): + 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): + """ + 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 + (where even single elements are 1-tuples). + """ + return tuple((elem,) for elem in topics) + + +async def test_eth_get_balance(session, root_signer, another_signer): + 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) + assert acc1_balance == to_transfer + + # Non-existent address (which is technically just an unfunded address) + random_addr = Address(os.urandom(20)) + balance = await session.rpc.eth_get_balance(random_addr) + assert balance == Amount.ether(0) + + +async def test_eth_get_transaction_receipt(local_provider, session, root_signer, another_signer): + local_provider.disable_auto_mine_transactions() + tx_hash = await session.broadcast_transfer( + root_signer, another_signer.address, Amount.ether(10) + ) + receipt = await session.rpc.eth_get_transaction_receipt(tx_hash) + assert receipt is None + + local_provider.enable_auto_mine_transactions() + receipt = await session.rpc.eth_get_transaction_receipt(tx_hash) + assert receipt.succeeded + + # A non-existent transaction + receipt = await session.rpc.eth_get_transaction_receipt(TxHash(os.urandom(32))) + assert receipt is None + + +async def test_eth_get_transaction_count(local_provider, session, root_signer, another_signer): + 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 + + # Check that pending transactions are accounted for + local_provider.disable_auto_mine_transactions() + await session.broadcast_transfer(root_signer, another_signer.address, Amount.ether(10)) + assert await session.rpc.eth_get_transaction_count(root_signer.address, BlockLabel.PENDING) == 2 + + +async def test_eth_gas_price(session): + gas_price = await session.rpc.eth_gas_price() + assert isinstance(gas_price, Amount) + + +async def test_eth_block_number(session, root_signer, another_signer): + 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.transactions[0].value == Amount.ether(2) + + +async def test_eth_get_block(session, root_signer, another_signer): + 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 all(isinstance(tx, TxInfo) for tx in block_info.transactions) + + 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 all(isinstance(tx, TxHash) for tx in block_info.transactions) + + # non-existent block + block_info = await session.rpc.eth_get_block_by_number(100, with_transactions=True) + assert block_info is None + block_info = await session.rpc.eth_get_block_by_hash( + BlockHash(b"\x00" * 32), with_transactions=True + ) + assert block_info is None + + +async def test_eth_get_block_pending(local_provider, session, root_signer, another_signer): + await session.transfer(root_signer, another_signer.address, Amount.ether(1)) + + local_provider.disable_auto_mine_transactions() + tx_hash = await session.broadcast_transfer( + root_signer, another_signer.address, Amount.ether(10) + ) + + block_info = await session.rpc.eth_get_block_by_number( + BlockLabel.PENDING, with_transactions=True + ) + 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 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 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): + 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.value == to_transfer + + non_existent = TxHash(b"abcd" * 8) + tx_info = await session.rpc.eth_get_transaction_by_hash(non_existent) + assert tx_info is None + + local_provider.disable_auto_mine_transactions() + 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.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): + 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) + + # The bytecode being deployed is not the code that will be stored on chain, + # but some code that, having been executed, returns the code that will be stored on chain. + # So all we can do is check that the code stored on chain is a part of the initialization code. + assert bytecode in compiled_contract.bytecode + + +async def test_eth_get_storage_at(session, root_signer, compiled_contracts): + x = 0xAB + y_key = Address(os.urandom(20)) + y_val = 0xCD + + compiled_contract = compiled_contracts["Storage"] + deployed_contract = await session.deploy( + root_signer, compiled_contract.constructor(x, y_key, y_val) + ) + + # Get the regular stored value + storage = await session.rpc.eth_get_storage_at( + deployed_contract.address, 0, block=BlockLabel.LATEST + ) + assert storage == b"\x00" * 31 + x.to_bytes(1, byteorder="big") + + # Get the value of the mapping + position = int.from_bytes( + keccak( + # left-padded key + b"\x00" * 12 + + bytes(y_key) + # left-padded position of the mapping (1) + + b"\x00" * 31 + + b"\x01" + ), + byteorder="big", + ) + storage = await session.rpc.eth_get_storage_at( + deployed_contract.address, position, block=BlockLabel.LATEST + ) + assert storage == b"\x00" * 31 + y_val.to_bytes(1, byteorder="big") + + +async def test_eth_call(session, compiled_contracts, root_signer, another_signer): + 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)) + assert result == (123 + 456,) + + # With a real provider, if `sender_address` is not given, it will default to the zero address. + result = await session.call(deployed_contract.method.getSender()) + assert result == (Address(b"\x00" * 20),) + + # Seems to be another tester chain limitation: even though `eth_call` does not spend gas, + # the `sender_address` still needs to be funded. + await session.transfer(root_signer, another_signer.address, Amount.ether(10)) + + result = await session.call( + deployed_contract.method.getSender(), sender_address=another_signer.address + ) + assert result == (another_signer.address,) + + +async def test_eth_call_pending(local_provider, session, compiled_contracts, root_signer): + compiled_contract = compiled_contracts["BasicContract"] + deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123)) + + local_provider.disable_auto_mine_transactions() + await session.broadcast_transact(root_signer, deployed_contract.method.setState(456)) + + # This uses the state of the last finalized block + result = await session.call(deployed_contract.method.getState(0)) + assert result == (123,) + + # This also uses the state change introduced by the pending transaction + result = await session.call(deployed_contract.method.getState(0), block=BlockLabel.PENDING) + assert result == (456,) + + +async def test_eth_get_filter_changes_bad_response(local_provider, session, monkeypatch): + block_filter = await session.rpc.eth_new_block_filter() + + def mock_rpc(_method, *_args): + return {"foo": 1} + + monkeypatch.setattr(local_provider, "rpc", mock_rpc) + + with pytest.raises( + BadResponseFormat, + match=r"eth_getFilterChanges: Can only structure a tuple or a list into a tuple generic", + ): + await session.rpc.eth_get_filter_changes(block_filter) + + +async def test_block_filter(session, root_signer, another_signer): + to_transfer = Amount.ether(1) + + await session.transfer(root_signer, another_signer.address, to_transfer) + + block_filter = await session.rpc.eth_new_block_filter() + + await session.transfer(root_signer, another_signer.address, to_transfer) + await session.transfer(root_signer, another_signer.address, to_transfer) + + last_block = await session.rpc.eth_get_block_by_number(BlockLabel.LATEST) + prev_block = await session.rpc.eth_get_block_by_number(last_block.number - 1) + + block_hashes = await session.rpc.eth_get_filter_changes(block_filter) + assert block_hashes == (prev_block.hash_, last_block.hash_) + + await session.transfer(root_signer, another_signer.address, to_transfer) + + block_hashes = await session.rpc.eth_get_filter_changes(block_filter) + last_block = await session.rpc.eth_get_block_by_number(BlockLabel.LATEST) + 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): + transaction_filter = await session.rpc.eth_new_pending_transaction_filter() + + to_transfer = Amount.ether(1) + + local_provider.disable_auto_mine_transactions() + tx_hash = await session.broadcast_transfer(root_signer, another_signer.address, to_transfer) + tx_hashes = await session.rpc.eth_get_filter_changes(transaction_filter) + assert tx_hashes == (tx_hash,) + + +async def test_eth_get_logs( + monkeypatch, local_provider, session, compiled_contracts, root_signer, another_signer +): + 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)) + contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) + await session.transact(root_signer, contract1.method.deposit(b"1234")) + await session.transact(another_signer, contract2.method.deposit2(b"4567")) + + entries = await session.rpc.eth_get_logs(source=contract2.address) + assert len(entries) == 1 + assert entries[0].address == contract2.address + assert ( + normalize_topics(entries[0].topics) + == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics + ) + + entries = await session.rpc.eth_get_logs( + source=[contract1.address, contract2.address], from_block=0 + ) + assert len(entries) == 2 + assert entries[0].address == contract1.address + assert entries[1].address == contract2.address + assert ( + normalize_topics(entries[0].topics) + == contract1.abi.event.Deposit(root_signer.address, b"1234").topics + ) + assert ( + normalize_topics(entries[1].topics) + == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics + ) + + # Test an invalid response + + def mock_rpc(_method, *_args): + return {"foo": 1} + + monkeypatch.setattr(local_provider, "rpc", mock_rpc) + + with pytest.raises( + BadResponseFormat, + match=r"eth_getLogs: Can only structure a tuple or a list into a tuple generic", + ): + await session.rpc.eth_get_logs(source=contract2.address) + + +async def test_eth_get_filter_logs(session, compiled_contracts, root_signer, another_signer): + 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)) + contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) + + log_filter = await session.rpc.eth_new_filter() + await session.transact(root_signer, contract1.method.deposit(b"1234")) + await session.transact(another_signer, contract2.method.deposit2(b"4567")) + + entries = await session.rpc.eth_get_filter_logs(log_filter) + assert len(entries) == 2 + assert entries[0].address == contract1.address + assert entries[1].address == contract2.address + + assert ( + normalize_topics(entries[0].topics) + == contract1.abi.event.Deposit(root_signer.address, b"1234").topics + ) + assert ( + normalize_topics(entries[1].topics) + == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics + ) + + +async def test_log_filter_all(session, compiled_contracts, root_signer, another_signer): + 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)) + contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) + + log_filter = await session.rpc.eth_new_filter() + await session.transact(root_signer, contract1.method.deposit(b"1234")) + await session.transact(another_signer, contract2.method.deposit2(b"4567")) + + entries = await session.rpc.eth_get_filter_changes(log_filter) + assert len(entries) == 2 + assert entries[0].address == contract1.address + assert entries[1].address == contract2.address + + assert ( + normalize_topics(entries[0].topics) + == contract1.abi.event.Deposit(root_signer.address, b"1234").topics + ) + assert ( + normalize_topics(entries[1].topics) + == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics + ) + + +async def test_log_filter_by_address(session, compiled_contracts, root_signer, another_signer): + 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)) + contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) + + # Filter by a single address + + log_filter = await session.rpc.eth_new_filter(source=contract2.address) + await session.transact(root_signer, contract1.method.deposit(b"1234")) + await session.transact(root_signer, contract2.method.deposit2(b"4567")) + + entries = await session.rpc.eth_get_filter_changes(log_filter) + assert len(entries) == 1 + assert entries[0].address == contract2.address + assert ( + normalize_topics(entries[0].topics) + == contract2.abi.event.Deposit2(root_signer.address, b"4567").topics + ) + + # Filter by several addresses + + contract3 = await session.deploy(another_signer, basic_contract.constructor(123)) + + log_filter = await session.rpc.eth_new_filter(source=[contract1.address, contract3.address]) + await session.transact(root_signer, contract1.method.deposit(b"1111")) + await session.transact(root_signer, contract2.method.deposit(b"2222")) + await session.transact(root_signer, contract3.method.deposit(b"3333")) + + entries = await session.rpc.eth_get_filter_changes(log_filter) + assert len(entries) == 2 + assert entries[0].address == contract1.address + assert ( + normalize_topics(entries[0].topics) + == contract1.abi.event.Deposit(root_signer.address, b"1111").topics + ) + assert entries[1].address == contract3.address + assert ( + normalize_topics(entries[1].topics) + == contract3.abi.event.Deposit(root_signer.address, b"3333").topics + ) + + +async def test_log_filter_by_topic(session, compiled_contracts, root_signer, another_signer): + 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)) + contract2 = await session.deploy(another_signer, basic_contract.constructor(123)) + + # Filter by a specific topic or None at each position + + log_filter = await session.rpc.eth_new_filter( + event_filter=contract2.abi.event.Deposit2(id=b"4567") + ) + # filtered out, wrong event type + await session.transact(root_signer, contract1.method.deposit(b"4567")) + # matches the filter + await session.transact(root_signer, contract1.method.deposit2(b"4567")) + # filtered out, wrong value + await session.transact(another_signer, contract2.method.deposit2(b"7890")) + # matches the filter + await session.transact(another_signer, contract2.method.deposit2(b"4567")) + + entries = await session.rpc.eth_get_filter_changes(log_filter) + assert len(entries) == 2 + assert entries[0].address == contract1.address + assert ( + normalize_topics(entries[0].topics) + == contract1.abi.event.Deposit2(root_signer.address, b"4567").topics + ) + assert entries[1].address == contract2.address + assert ( + normalize_topics(entries[1].topics) + == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics + ) + + # Filter by a list of topics + + event_filter = contract1.abi.event.Deposit(id=Either(b"1111", b"3333")) + log_filter = await session.rpc.eth_new_filter(event_filter=event_filter) + await session.transact(root_signer, contract1.method.deposit(b"1111")) + await session.transact(root_signer, contract1.method.deposit2(b"1111")) # filtered out + await session.transact(root_signer, contract1.method.deposit(b"2222")) # filtered out + await session.transact(another_signer, contract2.method.deposit(b"3333")) + + entries = await session.rpc.eth_get_filter_changes(log_filter) + assert len(entries) == 2 + assert entries[0].address == contract1.address + assert ( + normalize_topics(entries[0].topics) + == contract1.abi.event.Deposit(root_signer.address, b"1111").topics + ) + assert entries[1].address == contract2.address + assert ( + normalize_topics(entries[1].topics) + == contract2.abi.event.Deposit(another_signer.address, b"3333").topics + ) + + +async def test_log_filter_by_block_num(session, compiled_contracts, root_signer, another_signer): + 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)) + + await session.transact(root_signer, contract1.method.deposit(b"1111")) + block_num = await session.rpc.eth_block_number() + log_filter = await session.rpc.eth_new_filter(from_block=block_num + 1, to_block=block_num + 3) + + await session.transact(root_signer, contract1.method.deposit(b"2222")) # filter will start here + await session.transact(root_signer, contract1.method.deposit(b"3333")) + await session.transact(root_signer, contract1.method.deposit(b"4444")) # filter will stop here + await session.transact(root_signer, contract1.method.deposit(b"5555")) + + 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): + # 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") + + monkeypatch.setattr(local_provider, "rpc", mock_rpc) + + with pytest.raises(ProviderError, match=r"Provider error \(666\): this method is possessed"): + await session.net_version() + + +async def check_rpc_error(awaitable, expected_code, expected_message, expected_data): + with pytest.raises(ProviderError) as exc: + await awaitable + + assert exc.value.code == expected_code + assert exc.value.message == expected_message + assert exc.value.data == expected_data + + +async def test_contract_exceptions(session, root_signer, compiled_contracts): + compiled_contract = compiled_contracts["TestErrors"] + contract = await session.deploy(root_signer, compiled_contract.constructor(999)) + + error_selector = keccak(b"Error(string)")[:4] + custom_error_selector = keccak(b"CustomError(uint256)")[:4] + + # `require(condition)` + kwargs = dict( + 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( + 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( + 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( + 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( + 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): + compiled_contract = compiled_contracts["TestErrors"] + contract = await session.deploy(root_signer, compiled_contract.constructor(999)) + + panic_selector = keccak(b"Panic(uint256)")[:4] + + await check_rpc_error( + session.rpc.eth_call(contract.method.viewPanic(0)), + expected_code=RPCErrorCode.EXECUTION_ERROR, + expected_message="execution reverted", + expected_data=panic_selector + encode_args((abi.uint(256), 0x01)), + ) + + await check_rpc_error( + session.rpc.eth_call(contract.method.viewPanic(1)), + expected_code=RPCErrorCode.EXECUTION_ERROR, + expected_message="execution reverted", + expected_data=panic_selector + encode_args((abi.uint(256), 0x11)), + ) diff --git a/tests/test_contract_functionality.py b/tests/test_contract_functionality.py index b6b9955..0413d70 100644 --- a/tests/test_contract_functionality.py +++ b/tests/test_contract_functionality.py @@ -30,7 +30,7 @@ async def test_empty_constructor(session, root_signer, compiled_contracts): deployed_contract = await session.deploy(root_signer, compiled_contract.constructor()) call = deployed_contract.method.getState(123) - result = await session.eth_call(call) + result = await session.call(call) assert result == (1 + 123,) @@ -43,21 +43,21 @@ async def test_basics(session, root_signer, another_signer, compiled_contracts): deployed_contract = await session.deploy(another_signer, call) # Check the state - assert await session.eth_call(deployed_contract.method.v1()) == (12345,) - assert await session.eth_call(deployed_contract.method.v2()) == (56789,) + assert await session.call(deployed_contract.method.v1()) == (12345,) + assert await session.call(deployed_contract.method.v2()) == (56789,) # Transact with the contract await session.transact(another_signer, deployed_contract.method.setState(111)) - assert await session.eth_call(deployed_contract.method.v1()) == (111,) + assert await session.call(deployed_contract.method.v1()) == (111,) # Call the contract - result = await session.eth_call(deployed_contract.method.getState(123)) + result = await session.call(deployed_contract.method.getState(123)) assert result == (111 + 123,) inner = dict(inner1=1, inner2=2) outer = dict(inner=inner, outer1=3) - result = await session.eth_call(deployed_contract.method.testStructs(inner, outer)) + result = await session.call(deployed_contract.method.testStructs(inner, outer)) assert result == (inner, outer) @@ -68,10 +68,10 @@ async def test_overloaded_method(session, root_signer, compiled_contracts): call = compiled_contract.constructor(12345, 56789) deployed_contract = await session.deploy(root_signer, call) - result = await session.eth_call(deployed_contract.method.overloaded(123)) + result = await session.call(deployed_contract.method.overloaded(123)) assert result == (12345 + 123,) - result = await session.eth_call(deployed_contract.method.overloaded(123, 456)) + result = await session.call(deployed_contract.method.overloaded(123, 456)) assert result == (456 + 123,) @@ -87,22 +87,22 @@ async def test_read_only_mode(session, root_signer, compiled_contracts): deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(value, 0)) # check the state - result = await session.eth_call(deployed_contract.method.getState(0)) + result = await session.call(deployed_contract.method.getState(0)) assert result == (value,) # this method will not modify the state since it's invoked via `eth_call`, # but it will return a value that reflects the ethereal "modified" state. - result = await session.eth_call(deployed_contract.method.setStateAndReturn(1)) + result = await session.call(deployed_contract.method.setStateAndReturn(1)) assert result == (value + 1,) - result = await session.eth_call(deployed_contract.method.getState(0)) + result = await session.call(deployed_contract.method.getState(0)) assert result == (value,) # Now invoke it as a transaction, allowing it to modify the state await session.transact(root_signer, deployed_contract.method.setStateAndReturn(1)) # The state is now modified - result = await session.eth_call(deployed_contract.method.getState(0)) + result = await session.call(deployed_contract.method.getState(0)) assert result == (value + 1,) @@ -146,12 +146,12 @@ async def test_abi_declaration(session, root_signer, another_signer, compiled_co # Call the contract - result = await session.eth_call(deployed_contract.method.getState(123)) + result = await session.call(deployed_contract.method.getState(123)) assert result == 111 + 123 # Note the lack of `[]` - we declared outputs as a single value inner = dict(inner1=1, inner2=2) outer = dict(inner=inner, outer1=3) - result = await session.eth_call(deployed_contract.method.testStructs(inner, outer)) + result = await session.call(deployed_contract.method.testStructs(inner, outer)) assert result == (inner, outer) @@ -173,11 +173,11 @@ async def test_complicated_event(session, root_signer, compiled_contracts): contract = await session.deploy(root_signer, basic_contract.constructor(12345, 56789)) - log_filter1 = await session.eth_new_filter(event_filter=event_filter) # filter by topics - log_filter2 = await session.eth_new_filter() # collect everything + log_filter1 = await session.rpc.eth_new_filter(event_filter=event_filter) # filter by topics + log_filter2 = await session.rpc.eth_new_filter() # collect everything await session.transact(root_signer, contract.method.emitComplicated()) - entries_filtered = await session.eth_get_filter_changes(log_filter1) - entries_all = await session.eth_get_filter_changes(log_filter2) + entries_filtered = await session.rpc.eth_get_filter_changes(log_filter1) + entries_all = await session.rpc.eth_get_filter_changes(log_filter2) assert len(entries_all) == 1 assert entries_filtered == entries_all diff --git a/tests/test_local_provider.py b/tests/test_local_provider.py index d3d034e..37a5c09 100644 --- a/tests/test_local_provider.py +++ b/tests/test_local_provider.py @@ -35,7 +35,7 @@ async def test_root_balance(): provider = LocalProvider(root_balance=amount) client = Client(provider=provider) async with client.session() as session: - assert await session.eth_get_balance(provider.root.address) == amount + assert await session.get_balance(provider.root.address) == amount async def test_auto_mine(provider, session, root_signer, another_signer): @@ -44,22 +44,22 @@ async def test_auto_mine(provider, session, root_signer, another_signer): # Auto-mininig is the default behavior tx_hash = await session.broadcast_transfer(root_signer, dest, amount) - receipt = await session.eth_get_transaction_receipt(tx_hash) + receipt = await session.rpc.eth_get_transaction_receipt(tx_hash) assert receipt.succeeded - assert await session.eth_get_balance(dest) == amount + assert await session.get_balance(dest) == amount # Disable auto-mining. Now broadcasting the transaction does not automatically finalize it. provider.disable_auto_mine_transactions() tx_hash = await session.broadcast_transfer(root_signer, dest, amount) - receipt = await session.eth_get_transaction_receipt(tx_hash) + receipt = await session.rpc.eth_get_transaction_receipt(tx_hash) assert receipt is None - assert await session.eth_get_balance(dest) == amount + assert await session.get_balance(dest) == amount # Enable auto-mining back. The pending transactions are added to the block. provider.enable_auto_mine_transactions() - receipt = await session.eth_get_transaction_receipt(tx_hash) + receipt = await session.rpc.eth_get_transaction_receipt(tx_hash) assert receipt.succeeded - assert await session.eth_get_balance(dest) == Amount.ether(2) + assert await session.get_balance(dest) == Amount.ether(2) async def test_snapshots(provider, session, root_signer, another_signer): @@ -70,10 +70,10 @@ async def test_snapshots(provider, session, root_signer, another_signer): await session.transfer(root_signer, dest, amount) snapshot_id = provider.take_snapshot() await session.transfer(root_signer, dest, amount) - assert await session.eth_get_balance(dest) == double_amount + assert await session.get_balance(dest) == double_amount provider.revert_to_snapshot(snapshot_id) - assert await session.eth_get_balance(dest) == amount + assert await session.get_balance(dest) == amount async def test_net_version(session): diff --git a/tests/test_multicall.py b/tests/test_multicall.py index 9778772..13cab9d 100644 --- a/tests/test_multicall.py +++ b/tests/test_multicall.py @@ -3,7 +3,7 @@ import pytest from ethereum_rpc import Amount -from pons import ContractLegacyError, Multicall, ProviderError, compile_contract_file +from pons import ContractLegacyError, Multicall, compile_contract_file @pytest.fixture @@ -32,7 +32,7 @@ async def target(session, root_signer, target_compiled): async def test_multicall_read(session, multicall, target): - results = await session.eth_call(multicall.aggregate([target.method.x1(), target.method.x2()])) + results = await session.call(multicall.aggregate([target.method.x1(), target.method.x2()])) assert results[0] == (True, (1,)) assert results[1] == (True, (2,)) @@ -41,18 +41,18 @@ async def test_multicall_write(session, root_signer, multicall, target): await session.transact( root_signer, multicall.aggregate([target.method.write1(3), target.method.write2(4)]) ) - assert await session.eth_call(target.method.x1()) == (3,) - assert await session.eth_call(target.method.x2()) == (4,) + assert await session.call(target.method.x1()) == (3,) + assert await session.call(target.method.x2()) == (4,) async def test_multicall_read_error(session, multicall, target): # For now `eth_call` does not decode contract errors, so we get the low-level one. - with pytest.raises(ProviderError, match="execution reverted"): - await session.eth_call( + with pytest.raises(ContractLegacyError, match="Multicall3: call failed"): + await session.call( multicall.aggregate([target.method.x1(), target.method.read_error()]), ) - results = await session.eth_call( + results = await session.call( multicall.aggregate([target.method.x1(), target.method.read_error()], allow_failure=True), ) assert results[0] == (True, (1,)) # successful call @@ -67,8 +67,8 @@ async def test_multicall_write_error(session, root_signer, multicall, target): ) # The values remained unchanged - assert await session.eth_call(target.method.x1()) == (1,) - assert await session.eth_call(target.method.x2()) == (2,) + assert await session.call(target.method.x1()) == (1,) + assert await session.call(target.method.x2()) == (2,) await session.transact( root_signer, @@ -78,14 +78,14 @@ async def test_multicall_write_error(session, root_signer, multicall, target): ) # The successful call updated the value - assert await session.eth_call(target.method.x1()) == (3,) + assert await session.call(target.method.x1()) == (3,) # The reverted call did not update the value - assert await session.eth_call(target.method.x2()) == (2,) + assert await session.call(target.method.x2()) == (2,) async def test_multicall_read_value(session, multicall, target): - results = await session.eth_call( + results = await session.call( multicall.aggregate_value( [(target.method.x1(), Amount.wei(0)), (target.method.x2(), Amount.wei(0))] ) @@ -105,8 +105,8 @@ async def test_multicall_write_value(session, root_signer, multicall, target): ), amount=Amount.wei(300), ) - assert await session.eth_call(target.method.x1()) == (100,) - assert await session.eth_call(target.method.x2()) == (200,) + assert await session.call(target.method.x1()) == (100,) + assert await session.call(target.method.x2()) == (200,) async def test_multicall_write_value_insufficient_funds(session, root_signer, multicall, target): diff --git a/tests/test_provider.py b/tests/test_provider.py index 67f7faf..b1c422b 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -63,7 +63,7 @@ async def test_unexpected_response_type( monkeypatch.setattr(local_provider, "rpc", lambda _method, *_args: "something") with pytest.raises(BadResponseFormat, match="Cannot structure into"): - await session.eth_get_transaction_receipt(tx_hash) + await session.rpc.eth_get_transaction_receipt(tx_hash) async def test_missing_field(local_provider, session, monkeypatch, root_signer, another_signer): @@ -82,7 +82,7 @@ def mock_rpc(method, *args): monkeypatch.setattr(local_provider, "rpc", mock_rpc) with pytest.raises(BadResponseFormat, match="status: Missing field"): - await session.eth_get_transaction_receipt(tx_hash) + await session.rpc.eth_get_transaction_receipt(tx_hash) async def test_none_instead_of_dict( @@ -98,7 +98,7 @@ async def test_none_instead_of_dict( # but we force it here, just in case. monkeypatch.setattr(local_provider, "rpc", lambda _method, *_args: None) - assert await session.eth_get_transaction_receipt(tx_hash) is None + assert await session.rpc.eth_get_transaction_receipt(tx_hash) is None async def test_non_json_response(local_provider, session, monkeypatch): diff --git a/tests/test_utils.py b/tests/test_utils.py index a8a2d89..4c6617f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,17 +18,17 @@ async def test_create(session, root_signer, compiled_contracts): # Try deploying the contract with different nonces - nonce = await session.eth_get_transaction_count(root_signer.address) + nonce = await session.rpc.eth_get_transaction_count(root_signer.address) assert nonce == 0 to_deploy = await session.deploy(root_signer, compiled_to_deploy.constructor(123)) assert to_deploy.address == get_create_address(root_signer.address, nonce) - nonce = await session.eth_get_transaction_count(root_signer.address) + nonce = await session.rpc.eth_get_transaction_count(root_signer.address) assert nonce == 1 to_deploy = await session.deploy(root_signer, compiled_to_deploy.constructor(123)) assert to_deploy.address == get_create_address(root_signer.address, nonce) - nonce = await session.eth_get_transaction_count(root_signer.address) + nonce = await session.rpc.eth_get_transaction_count(root_signer.address) assert nonce == 2 to_deploy = await session.deploy(root_signer, compiled_to_deploy.constructor(123)) assert to_deploy.address == get_create_address(root_signer.address, nonce)