From a9865ee3b52d08d1e816cadb30f430694a4b59a9 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 26 Oct 2025 10:32:05 -0700 Subject: [PATCH 1/9] Add `eth_accounts` method --- alysis/_node.py | 7 +++++++ alysis/_rpc.py | 5 +++++ tests/test_rpc.py | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/alysis/_node.py b/alysis/_node.py index 8a977e4..ab8f8d7 100644 --- a/alysis/_node.py +++ b/alysis/_node.py @@ -437,3 +437,10 @@ def eth_get_filter_logs(self, filter_id: int) -> list[LogEntry]: raise FilterNotFound("Unknown filter id") return self._get_logs(log_filter) + + 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 [] diff --git a/alysis/_rpc.py b/alysis/_rpc.py index e5f3e1f..e827702 100644 --- a/alysis/_rpc.py +++ b/alysis/_rpc.py @@ -38,6 +38,7 @@ def __init__(self, node: Node): self.node = node self._methods = dict( net_version=self._net_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, @@ -213,3 +214,7 @@ 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_accounts(self, params: tuple[JSON, ...]) -> JSON: + _ = structure(tuple[()], params) + return unstructure(self.node.eth_accounts(), list[Address]) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 96e1dd7..ede04bf 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -15,3 +15,7 @@ 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") == [] From ed76cc1fcfaea785aea6c661083a70fd4a1e108d Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 26 Oct 2025 10:33:41 -0700 Subject: [PATCH 2/9] Add `web3_clientVersion` method --- alysis/_node.py | 3 +++ alysis/_rpc.py | 5 +++++ tests/test_rpc.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/alysis/_node.py b/alysis/_node.py index ab8f8d7..6cd3c28 100644 --- a/alysis/_node.py +++ b/alysis/_node.py @@ -207,6 +207,9 @@ def net_version(self) -> int: """Returns the current network id.""" return self._net_version + def web3_client_version(self) -> str: + return "Alysis testerchain" + def eth_chain_id(self) -> int: """Returns the chain ID used for signing replay-protected transactions.""" return self._backend.chain_id diff --git a/alysis/_rpc.py b/alysis/_rpc.py index e827702..60ce82f 100644 --- a/alysis/_rpc.py +++ b/alysis/_rpc.py @@ -38,6 +38,7 @@ 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, @@ -112,6 +113,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()) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index ede04bf..7871d73 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -19,3 +19,7 @@ def test_eth_get_balance(rpc_node, root_account, another_account): 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" From c8a58cbc5c264b4cdadb834f58cd51bce2b28685 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 26 Oct 2025 10:14:31 -0700 Subject: [PATCH 3/9] Add `web3_sha3` method --- alysis/_node.py | 4 ++++ alysis/_rpc.py | 5 +++++ tests/test_rpc.py | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/alysis/_node.py b/alysis/_node.py index 6cd3c28..2ae7048 100644 --- a/alysis/_node.py +++ b/alysis/_node.py @@ -16,6 +16,7 @@ TxHash, TxInfo, TxReceipt, + keccak, ) from ._backend import PyEVMBackend @@ -210,6 +211,9 @@ def net_version(self) -> int: 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 diff --git a/alysis/_rpc.py b/alysis/_rpc.py index 60ce82f..fb657eb 100644 --- a/alysis/_rpc.py +++ b/alysis/_rpc.py @@ -60,6 +60,7 @@ 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, + web3_sha3=self._web3_sha3, ) def rpc(self, method_name: str, *params: JSON) -> JSON: @@ -223,3 +224,7 @@ def _eth_get_logs(self, params: tuple[JSON, ...]) -> JSON: 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)) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 7871d73..348c53c 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -23,3 +23,10 @@ def test_eth_accounts(rpc_node): 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" + ) From cac18dd8450155ef582a3a49966cbb077c102a24 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 26 Oct 2025 10:38:22 -0700 Subject: [PATCH 4/9] Add `net_listening` method --- alysis/_node.py | 4 ++++ alysis/_rpc.py | 5 +++++ tests/test_rpc.py | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/alysis/_node.py b/alysis/_node.py index 2ae7048..fd9de83 100644 --- a/alysis/_node.py +++ b/alysis/_node.py @@ -451,3 +451,7 @@ def eth_accounts(self) -> list[Address]: # # 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 diff --git a/alysis/_rpc.py b/alysis/_rpc.py index fb657eb..1b80ea3 100644 --- a/alysis/_rpc.py +++ b/alysis/_rpc.py @@ -61,6 +61,7 @@ def __init__(self, node: Node): eth_getLogs=self._eth_get_logs, eth_getFilterLogs=self._eth_get_filter_logs, web3_sha3=self._web3_sha3, + net_listening=self._net_listening, ) def rpc(self, method_name: str, *params: JSON) -> JSON: @@ -228,3 +229,7 @@ def _eth_accounts(self, params: tuple[JSON, ...]) -> JSON: 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()) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 348c53c..823f586 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -30,3 +30,7 @@ def test_web3_sha3(rpc_node): rpc_node.rpc("web3_sha3", "0x68656c6c6f20776f726c64") == "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" ) + + +def test_net_listening(rpc_node): + assert rpc_node.rpc("net_listening") From 99436add374c39e50492265789362472b0776a15 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 26 Oct 2025 10:42:46 -0700 Subject: [PATCH 5/9] Add `net_peerCount` method --- alysis/_node.py | 5 +++++ alysis/_rpc.py | 5 +++++ tests/test_rpc.py | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/alysis/_node.py b/alysis/_node.py index fd9de83..353db94 100644 --- a/alysis/_node.py +++ b/alysis/_node.py @@ -455,3 +455,8 @@ def eth_accounts(self) -> list[Address]: 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 diff --git a/alysis/_rpc.py b/alysis/_rpc.py index 1b80ea3..8af4769 100644 --- a/alysis/_rpc.py +++ b/alysis/_rpc.py @@ -62,6 +62,7 @@ def __init__(self, node: Node): eth_getFilterLogs=self._eth_get_filter_logs, web3_sha3=self._web3_sha3, net_listening=self._net_listening, + net_peerCount=self._net_peer_count, ) def rpc(self, method_name: str, *params: JSON) -> JSON: @@ -233,3 +234,7 @@ def _web3_sha3(self, params: tuple[JSON, ...]) -> JSON: 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()) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 823f586..de2af30 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -34,3 +34,7 @@ def test_web3_sha3(rpc_node): 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) From 7011a726e5eee85653c2a1250a6e3f5672fbe097 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 26 Oct 2025 10:50:08 -0700 Subject: [PATCH 6/9] Add `eth_coinbase` method --- alysis/_backend.py | 6 ++++++ alysis/_node.py | 3 +++ alysis/_rpc.py | 5 +++++ tests/test_rpc.py | 4 ++++ 4 files changed, 18 insertions(+) diff --git a/alysis/_backend.py b/alysis/_backend.py index fe75226..6020204 100644 --- a/alysis/_backend.py +++ b/alysis/_backend.py @@ -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 diff --git a/alysis/_node.py b/alysis/_node.py index 353db94..08e970d 100644 --- a/alysis/_node.py +++ b/alysis/_node.py @@ -460,3 +460,6 @@ 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 diff --git a/alysis/_rpc.py b/alysis/_rpc.py index 8af4769..136d461 100644 --- a/alysis/_rpc.py +++ b/alysis/_rpc.py @@ -63,6 +63,7 @@ def __init__(self, node: Node): web3_sha3=self._web3_sha3, net_listening=self._net_listening, net_peerCount=self._net_peer_count, + eth_coinbase=self._eth_coinbase, ) def rpc(self, method_name: str, *params: JSON) -> JSON: @@ -238,3 +239,7 @@ def _net_listening(self, params: tuple[JSON, ...]) -> JSON: 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()) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index de2af30..4473c7d 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -38,3 +38,7 @@ def test_net_listening(rpc_node): 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() From c1318132389ecc2479fa177995e19afa8b82f6dc Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 26 Oct 2025 15:38:59 -0700 Subject: [PATCH 7/9] Add various tx/block query methods --- alysis/__init__.py | 2 ++ alysis/_exceptions.py | 4 ++++ alysis/_node.py | 52 +++++++++++++++++++++++++++++++++++++++++-- alysis/_rpc.py | 50 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/alysis/__init__.py b/alysis/__init__.py index ec0c35d..52efb09 100644 --- a/alysis/__init__.py +++ b/alysis/__init__.py @@ -4,6 +4,7 @@ from ._exceptions import ( BlockNotFound, FilterNotFound, + IndexNotFound, TransactionFailed, TransactionNotFound, TransactionReverted, @@ -17,6 +18,7 @@ "EVMVersion", "FilterNotFound", "FilterParams", + "IndexNotFound", "Node", "RPCNode", "TransactionFailed", diff --git a/alysis/_exceptions.py b/alysis/_exceptions.py index ddc60b0..de7cef2 100644 --- a/alysis/_exceptions.py +++ b/alysis/_exceptions.py @@ -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.""" diff --git a/alysis/_node.py b/alysis/_node.py index 08e970d..beb972e 100644 --- a/alysis/_node.py +++ b/alysis/_node.py @@ -1,5 +1,5 @@ from copy import deepcopy -from typing import Any +from typing import Any, cast from ethereum_rpc import ( Address, @@ -21,7 +21,7 @@ from ._backend import PyEVMBackend from ._constants import EVMVersion -from ._exceptions import FilterNotFound, ValidationError +from ._exceptions import FilterNotFound, IndexNotFound, ValidationError class LogFilter: @@ -463,3 +463,51 @@ def net_peer_count(self) -> int: 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) diff --git a/alysis/_rpc.py b/alysis/_rpc.py index 136d461..7cf92ae 100644 --- a/alysis/_rpc.py +++ b/alysis/_rpc.py @@ -6,6 +6,7 @@ Address, Block, BlockHash, + BlockInfo, EstimateGasParams, EthCallParams, FilterParams, @@ -20,6 +21,7 @@ from ._exceptions import ( BlockNotFound, + IndexNotFound, TransactionFailed, TransactionNotFound, TransactionReverted, @@ -64,6 +66,14 @@ def __init__(self, node: Node): 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: @@ -83,6 +93,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 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 @@ -243,3 +257,39 @@ def _net_peer_count(self, params: tuple[JSON, ...]) -> JSON: 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 + ) From ef09d0e30b12e16bdede7b1b275089a0072775be Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sun, 26 Oct 2025 11:58:42 -0700 Subject: [PATCH 8/9] Add `eth_uninstallFilter` method --- alysis/_node.py | 18 +++++++++++++++--- alysis/_rpc.py | 14 +++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/alysis/_node.py b/alysis/_node.py index beb972e..8cfb063 100644 --- a/alysis/_node.py +++ b/alysis/_node.py @@ -377,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 @@ -406,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 = [] @@ -441,10 +441,22 @@ 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). diff --git a/alysis/_rpc.py b/alysis/_rpc.py index 7cf92ae..21ba702 100644 --- a/alysis/_rpc.py +++ b/alysis/_rpc.py @@ -21,6 +21,7 @@ from ._exceptions import ( BlockNotFound, + FilterNotFound, IndexNotFound, TransactionFailed, TransactionNotFound, @@ -62,6 +63,7 @@ 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, @@ -93,7 +95,7 @@ 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 IndexNotFound as 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 @@ -238,6 +240,16 @@ 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]) From 0d6e604cb45e3ddde842517924300ebd9e2be825 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 27 Oct 2025 10:21:40 -0700 Subject: [PATCH 9/9] Add changelog entry --- docs/changelog.rst | 15 +++++++++++++++ pyproject.toml | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 63e944c..a48d9b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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) ------------------ diff --git a/pyproject.toml b/pyproject.toml index 3c1e12a..7aaa006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"},