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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions alysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ._exceptions import (
BlockNotFound,
FilterNotFound,
IndexNotFound,
TransactionFailed,
TransactionNotFound,
TransactionReverted,
Expand All @@ -17,6 +18,7 @@
"EVMVersion",
"FilterNotFound",
"FilterParams",
"IndexNotFound",
"Node",
"RPCNode",
"TransactionFailed",
Expand Down
6 changes: 6 additions & 0 deletions alysis/_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ def __deepcopy__(self, _memo: None | dict[Any, Any]) -> "PyEVMBackend":
obj._initialize(copy_chain(self.chain), self.root_private_key, self._total_difficulty) # noqa: SLF001
return obj

@property
def coinbase(self) -> Address:
# Don't see an easy way to get it out of PyEVM,
# but that's what we supply in the genesis parameters.
return Address(ZERO_ADDRESS)

def mine_block(self, timestamp: None | int = None) -> BlockHash:
if timestamp is not None:
current_timestamp = self.chain.header.timestamp
Expand Down
4 changes: 4 additions & 0 deletions alysis/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class TransactionNotFound(Exception):
"""Requested transaction cannot be found."""


class IndexNotFound(Exception):
"""Requested index is outside of the available range."""


class FilterNotFound(Exception):
"""Requested filter cannot be found."""

Expand Down
96 changes: 91 additions & 5 deletions alysis/_node.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from copy import deepcopy
from typing import Any
from typing import Any, cast

from ethereum_rpc import (
Address,
Expand All @@ -16,11 +16,12 @@
TxHash,
TxInfo,
TxReceipt,
keccak,
)

from ._backend import PyEVMBackend
from ._constants import EVMVersion
from ._exceptions import FilterNotFound, ValidationError
from ._exceptions import FilterNotFound, IndexNotFound, ValidationError


class LogFilter:
Expand Down Expand Up @@ -207,6 +208,12 @@ def net_version(self) -> int:
"""Returns the current network id."""
return self._net_version

def web3_client_version(self) -> str:
return "Alysis testerchain"

def web3_sha3(self, data: bytes) -> bytes:
return keccak(data)

def eth_chain_id(self) -> int:
"""Returns the chain ID used for signing replay-protected transactions."""
return self._backend.chain_id
Expand Down Expand Up @@ -370,7 +377,7 @@ def delete_filter(self, filter_id: int) -> None:
elif filter_id in self._log_filters:
del self._log_filters[filter_id]
else:
raise FilterNotFound("Unknown filter id")
raise FilterNotFound(f"Unknown filter id: {filter_id}")

def eth_get_filter_changes(
self, filter_id: int
Expand Down Expand Up @@ -399,7 +406,7 @@ def eth_get_filter_changes(
self._log_filter_entries[filter_id] = []
return log_entries

raise FilterNotFound("Unknown filter id")
raise FilterNotFound(f"Unknown filter id: {filter_id}")

def _get_logs(self, log_filter: LogFilter) -> list[LogEntry]:
entries = []
Expand Down Expand Up @@ -434,6 +441,85 @@ def eth_get_filter_logs(self, filter_id: int) -> list[LogEntry]:
if filter_id in self._log_filters:
log_filter = self._log_filters[filter_id]
else:
raise FilterNotFound("Unknown filter id")
raise FilterNotFound(f"Unknown filter id: {filter_id}")

return self._get_logs(log_filter)

def eth_uninstall_filter(self, filter_id: int) -> None:
if filter_id in self._log_filters:
del self._log_filters[filter_id]
return
if filter_id in self._block_filters:
del self._block_filters[filter_id]
return
if filter_id in self._pending_transaction_filters:
del self._pending_transaction_filters[filter_id]
return
raise FilterNotFound(f"Unknown filter id: {filter_id}")

def eth_accounts(self) -> list[Address]:
# Returning an empty list allows us to not implement the related methods
# (eth_sign, eth_signTransaction, eth_sendTransaction).
#
# It is generally not a good idea to keep private keys on the provider's side anyway.
return []

def net_listening(self) -> bool:
# That's what a real provider would return.
return True

def net_peer_count(self) -> int:
# Returning a non-zero number in case there's some application
# that uses it to test the provider's liveness or something.
return 42

def eth_coinbase(self) -> Address:
return self._backend.coinbase

def eth_get_block_transaction_count_by_hash(self, block_hash: BlockHash) -> int:
return len(self.eth_get_block_by_hash(block_hash, with_transactions=False).transactions)

def eth_get_block_transaction_count_by_number(self, block: Block) -> int:
return len(self.eth_get_block_by_number(block, with_transactions=False).transactions)

def eth_get_uncle_count_by_block_hash(self, block_hash: BlockHash) -> int:
return len(self.eth_get_block_by_hash(block_hash, with_transactions=False).uncles)

def eth_get_uncle_count_by_block_number(self, block: Block) -> int:
return len(self.eth_get_block_by_number(block, with_transactions=False).uncles)

def eth_get_transaction_by_block_hash_and_index(
self, block_hash: BlockHash, index: int
) -> TxInfo:
block_info = self.eth_get_block_by_hash(block_hash, with_transactions=True)
if index < 0 or index >= len(block_info.transactions):
raise IndexNotFound(
f"{len(block_info.transactions)} transactions available, requested index {index}"
)
# Can cast here since we requested a block with transactions above
return cast("TxInfo", block_info.transactions[index])

def eth_get_transaction_by_block_number_and_index(self, block: Block, index: int) -> TxInfo:
block_info = self.eth_get_block_by_number(block, with_transactions=True)
if index < 0 or index >= len(block_info.transactions):
raise IndexNotFound(
f"{len(block_info.transactions)} transactions available, requested index {index}"
)
# Can cast here since we requested a block with transactions above
return cast("TxInfo", block_info.transactions[index])

def eth_get_uncle_by_block_hash_and_index(
self, block_hash: BlockHash, index: int
) -> BlockInfo | None:
block_info = self.eth_get_block_by_hash(block_hash, with_transactions=False)
if index < 0 or index >= len(block_info.uncles):
# Following the behavior of the providers
return None
return self.eth_get_block_by_hash(block_info.uncles[index], with_transactions=False)

def eth_get_uncle_by_block_number_and_index(self, block: Block, index: int) -> BlockInfo | None:
block_info = self.eth_get_block_by_number(block, with_transactions=False)
if index < 0 or index >= len(block_info.uncles):
# Following the behavior of the providers
return None
return self.eth_get_block_by_hash(block_info.uncles[index], with_transactions=False)
92 changes: 92 additions & 0 deletions alysis/_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Address,
Block,
BlockHash,
BlockInfo,
EstimateGasParams,
EthCallParams,
FilterParams,
Expand All @@ -20,6 +21,8 @@

from ._exceptions import (
BlockNotFound,
FilterNotFound,
IndexNotFound,
TransactionFailed,
TransactionNotFound,
TransactionReverted,
Expand All @@ -38,6 +41,8 @@ def __init__(self, node: Node):
self.node = node
self._methods = dict(
net_version=self._net_version,
web3_clientVersion=self._web3_client_version,
eth_accounts=self._eth_accounts,
eth_chainId=self._eth_chain_id,
eth_getBalance=self._eth_get_balance,
eth_getTransactionReceipt=self._eth_get_transaction_receipt,
Expand All @@ -58,6 +63,19 @@ def __init__(self, node: Node):
eth_getFilterChanges=self._eth_get_filter_changes,
eth_getLogs=self._eth_get_logs,
eth_getFilterLogs=self._eth_get_filter_logs,
eth_uninstallFilter=self._eth_uninstall_filter,
web3_sha3=self._web3_sha3,
net_listening=self._net_listening,
net_peerCount=self._net_peer_count,
eth_coinbase=self._eth_coinbase,
eth_getBlockTransactionCountByHash=self._eth_get_block_transaction_count_by_hash,
eth_getBlockTransactionCountByNumber=self._eth_get_block_transaction_count_by_number,
eth_getUncleCountByBlockHash=self._eth_get_uncle_count_by_block_hash,
eth_getUncleCountByBlockNumber=self._eth_get_uncle_count_by_block_number,
eth_getTransactionByBlockHashAndIndex=self._eth_get_transaction_by_block_hash_and_index,
eth_getTransactionByBlockNumberAndIndex=self._eth_get_transaction_by_block_number_and_index,
eth_getUncleByBlockHashAndIndex=self._eth_get_uncle_by_block_hash_and_index,
eth_getUncleByBlockNumberAndIndex=self._eth_get_uncle_by_block_number_and_index,
)

def rpc(self, method_name: str, *params: JSON) -> JSON:
Expand All @@ -77,6 +95,10 @@ def rpc(self, method_name: str, *params: JSON) -> JSON:
# If we didn't process it earlier, it's a SERVER_ERROR
raise RPCError.with_code(RPCErrorCode.SERVER_ERROR, str(exc)) from exc

except (FilterNotFound, IndexNotFound) as exc:
# That's what the providers seem to return.
raise RPCError.with_code(RPCErrorCode.METHOD_NOT_FOUND, str(exc)) from exc

except (StructuringError, ValidationError) as exc:
raise RPCError.with_code(RPCErrorCode.INVALID_PARAMETER, str(exc)) from exc

Expand Down Expand Up @@ -111,6 +133,10 @@ def _net_version(self, params: tuple[JSON, ...]) -> JSON:
# Note: it's not hex encoded, but just stringified!
return str(self.node.net_version())

def _web3_client_version(self, params: tuple[JSON, ...]) -> JSON:
_ = structure(tuple[()], params)
return self.node.web3_client_version()

def _eth_chain_id(self, params: tuple[JSON, ...]) -> JSON:
_ = structure(tuple[()], params)
return unstructure(self.node.eth_chain_id())
Expand Down Expand Up @@ -213,3 +239,69 @@ def _eth_get_filter_logs(self, params: tuple[JSON, ...]) -> JSON:
def _eth_get_logs(self, params: tuple[JSON, ...]) -> JSON:
(typed_params,) = structure(tuple[FilterParams | FilterParamsEIP234], params)
return unstructure(self.node.eth_get_logs(typed_params), list[LogEntry])

def _eth_uninstall_filter(self, params: tuple[JSON, ...]) -> JSON:
(filter_id,) = structure(tuple[int], params)
# Unlike other filter operations, a non-existent filter does not cause an error.
try:
self.node.eth_uninstall_filter(filter_id)
result = True
except FilterNotFound:
result = False
return unstructure(result)

def _eth_accounts(self, params: tuple[JSON, ...]) -> JSON:
_ = structure(tuple[()], params)
return unstructure(self.node.eth_accounts(), list[Address])

def _web3_sha3(self, params: tuple[JSON, ...]) -> JSON:
(data,) = structure(tuple[bytes], params)
return unstructure(self.node.web3_sha3(data))

def _net_listening(self, params: tuple[JSON, ...]) -> JSON:
_ = structure(tuple[()], params)
return unstructure(self.node.net_listening())

def _net_peer_count(self, params: tuple[JSON, ...]) -> JSON:
_ = structure(tuple[()], params)
return unstructure(self.node.net_peer_count())

def _eth_coinbase(self, params: tuple[JSON, ...]) -> JSON:
_ = structure(tuple[()], params)
return unstructure(self.node.eth_coinbase())

def _eth_get_block_transaction_count_by_hash(self, params: tuple[JSON, ...]) -> JSON:
(block_hash,) = structure(tuple[BlockHash], params)
return unstructure(self.node.eth_get_block_transaction_count_by_hash(block_hash))

def _eth_get_block_transaction_count_by_number(self, params: tuple[JSON, ...]) -> JSON:
(block,) = structure(tuple[Block], params)
return unstructure(self.node.eth_get_block_transaction_count_by_number(block))

def _eth_get_uncle_count_by_block_hash(self, params: tuple[JSON, ...]) -> JSON:
(block_hash,) = structure(tuple[BlockHash], params)
return unstructure(self.node.eth_get_uncle_count_by_block_hash(block_hash))

def _eth_get_uncle_count_by_block_number(self, params: tuple[JSON, ...]) -> JSON:
(block,) = structure(tuple[Block], params)
return unstructure(self.node.eth_get_uncle_count_by_block_number(block))

def _eth_get_transaction_by_block_hash_and_index(self, params: tuple[JSON, ...]) -> JSON:
(block_hash, index) = structure(tuple[BlockHash, int], params)
return unstructure(self.node.eth_get_transaction_by_block_hash_and_index(block_hash, index))

def _eth_get_transaction_by_block_number_and_index(self, params: tuple[JSON, ...]) -> JSON:
(block, index) = structure(tuple[Block, int], params)
return unstructure(self.node.eth_get_transaction_by_block_number_and_index(block, index))

def _eth_get_uncle_by_block_hash_and_index(self, params: tuple[JSON, ...]) -> JSON:
(block_hash, index) = structure(tuple[BlockHash, int], params)
return unstructure(
self.node.eth_get_uncle_by_block_hash_and_index(block_hash, index), BlockInfo | None
)

def _eth_get_uncle_by_block_number_and_index(self, params: tuple[JSON, ...]) -> JSON:
(block, index) = structure(tuple[Block, int], params)
return unstructure(
self.node.eth_get_uncle_by_block_number_and_index(block, index), BlockInfo | None
)
15 changes: 15 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
Changelog
=========


0.6.3 (in development)
----------------------

Added
^^^^^

- ``eth_accounts``, ``web3_clientVersion``, ``net_listening``, ``net_peerCount``, ``eth_coinbase``, ``eth_uninstallFilter``, ``eth_getBlockTransactionCountByHash``, ``eth_getBlockTransactionCountByNumber``, ``eth_getUncleCountByBlockHash``, ``eth_getUncleCountByBlockNumber``, ``eth_getTransactionByBlockHashAndIndex``, ``eth_getTransactionByBlockNumberAndIndexdex``, ``eth_getUncleByBlockHashAndIndex``, ``eth_getUncleByBlockNumberAndIndex``. (PR_29_)
- ``IndexNotFound`` exception. (PR_29_)


.. _PR_29: https://github.com/fjarri/compages/pull/29



0.6.2 (2025-19-10)
------------------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "alysis"
version = "0.6.2"
version = "0.6.3-dev"
description = "Ethereum testerchain"
authors = [
{name = "Bogdan Opanchuk", email = "bogdan@opanchuk.net"},
Expand Down
27 changes: 27 additions & 0 deletions tests/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,30 @@ def test_eth_get_balance(rpc_node, root_account, another_account):

result = rpc_node.rpc("eth_getBalance", another_account.address, "latest")
assert result == hex(10**9)


def test_eth_accounts(rpc_node):
assert rpc_node.rpc("eth_accounts") == []


def test_web3_client_version(rpc_node):
assert rpc_node.rpc("web3_clientVersion") == "Alysis testerchain"


def test_web3_sha3(rpc_node):
assert (
rpc_node.rpc("web3_sha3", "0x68656c6c6f20776f726c64")
== "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"
)


def test_net_listening(rpc_node):
assert rpc_node.rpc("net_listening")


def test_net_peer_count(rpc_node):
assert rpc_node.rpc("net_peerCount") == hex(42)


def test_eth_coinbase(rpc_node):
assert rpc_node.rpc("eth_coinbase") == "0x" + (20 * b"\x00").hex()