From 6d6459c576d850e2b58a7035019ea48c59a84779 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 10 Feb 2024 12:23:20 -0800 Subject: [PATCH 1/6] Fix a typo in `eth_getFilterChanges` docs --- pons/_client.py | 4 ++-- tests/test_client.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pons/_client.py b/pons/_client.py index 296f8b3..62c6cf1 100644 --- a/pons/_client.py +++ b/pons/_client.py @@ -785,12 +785,12 @@ async def eth_new_filter( filter_id = LogFilterId.rpc_decode(result) return LogFilter(id_=filter_id, provider_path=provider_path) - @rpc_call("eth_getFilterChangers") + @rpc_call("eth_getFilterChanges") async def eth_get_filter_changes( self, filter_: Union[BlockFilter, PendingTransactionFilter, LogFilter] ) -> Union[Tuple[BlockHash, ...], Tuple[TxHash, ...], Tuple[LogEntry, ...]]: """ - Calls the ``eth_getFilterChangers`` RPC method. + Calls the ``eth_getFilterChanges`` RPC method. Depending on what ``filter_`` was, returns a tuple of corresponding results. """ # TODO: split into separate functions with specific return types? diff --git a/tests/test_client.py b/tests/test_client.py index 4d33db7..87e5c1f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -580,7 +580,7 @@ async def test_eth_get_filter_changes_bad_response(local_provider, session, monk block_filter = await session.eth_new_block_filter() with pytest.raises( - BadResponseFormat, match=r"eth_getFilterChangers: Expected a list as a response, got dict" + BadResponseFormat, match=r"eth_getFilterChanges: Expected a list as a response, got dict" ): await session.eth_get_filter_changes(block_filter) From 52d263edcb7956507fb0a26be3005ea012912cd6 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 10 Feb 2024 12:23:27 -0800 Subject: [PATCH 2/6] Fix formatting --- tests/test_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 87e5c1f..d29b9c8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -490,9 +490,7 @@ async def test_get_block_pending(local_provider, session, root_signer, another_s await session.transfer(root_signer, another_signer.address, Amount.ether(1)) local_provider.disable_auto_mine_transactions() - await session.broadcast_transfer( - root_signer, another_signer.address, Amount.ether(10) - ) + await session.broadcast_transfer(root_signer, another_signer.address, Amount.ether(10)) block_info = await session.eth_get_block_by_number(Block.PENDING, with_transactions=True) assert len(block_info.transactions) == 0 From b4362e90b88fc16346800e2235133e8a3cc158ec Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 10 Feb 2024 12:23:48 -0800 Subject: [PATCH 3/6] Fix formatting in the changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 54c4743..c18ff68 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,7 @@ Changelog ~~~~~~~~~~~~~~~~~~ Changed -~~~~~~~ +^^^^^^^ - Added an explicit ``typing_extensions`` dependency. (PR_57_) - Various boolean arguments are now keyword-only to prevent usage errors. (PR_57_) From 039c2ee99a79401f365c9e035192ba0dd0c02070 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 10 Feb 2024 12:26:30 -0800 Subject: [PATCH 4/6] Support `logs` field in `TxReceipt` --- docs/changelog.rst | 2 + pons/_entities.py | 129 +++++++++++++++++++++------------------- pons/_local_provider.py | 5 ++ tests/test_entities.py | 17 ++++++ 4 files changed, 92 insertions(+), 61 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c18ff68..abdc268 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -30,6 +30,7 @@ Added - Expose ``Provider`` at the top level. (PR_63_) - ``eth_getCode`` support (as ``ClientSession.eth_get_code()``). (PR_64_) - ``eth_getStorageAt`` support (as ``ClientSession.eth_get_storage_at()``). (PR_64_) +- Support for the ``logs`` field in ``TxReceipt``. (PR_68_) Fixed @@ -54,6 +55,7 @@ Fixed .. _PR_64: https://github.com/fjarri/pons/pull/64 .. _PR_65: https://github.com/fjarri/pons/pull/65 .. _PR_67: https://github.com/fjarri/pons/pull/67 +.. _PR_68: https://github.com/fjarri/pons/pull/68 0.7.0 (09-07-2023) diff --git a/pons/_entities.py b/pons/_entities.py index c3f7e20..710855b 100644 --- a/pons/_entities.py +++ b/pons/_entities.py @@ -450,6 +450,67 @@ def rpc_decode(cls, val: ResponseDict) -> "BlockInfo": ) +class LogEntry(NamedTuple): + """Log entry metadata.""" + + removed: bool + """ + ``True`` if log was removed, due to a chain reorganization. + ``False`` if it is a valid log. + """ + + address: Address + """ + The contract address from which this log originated. + """ + + data: bytes + """ABI-packed non-indexed arguments of the event.""" + + topics: Tuple[LogTopic, ...] + """ + Values of indexed event fields. + For a named event, the first topic is the event's selector. + """ + + # In the docs of major providers (Infura, Alchemy, Quicknode) it is claimed + # that the following fields can be null if "it is a pending log". + # I could not reproduce such behavior, so for now they're staying non-nullable. + + log_index: int + """Log's position in the block.""" + + transaction_index: int + """Transaction's position in the block.""" + + transaction_hash: TxHash + """Hash of the transactions this log was created from.""" + + block_hash: BlockHash + """Hash of the block where this log was in.""" + + block_number: int + """The block number where this log was.""" + + @classmethod + def rpc_decode(cls, val: ResponseDict) -> "LogEntry": + topics = val["topics"] + if not isinstance(topics, Iterable): + raise RPCDecodingError(f"`topics` in a log entry must be an iterable, got {topics}") + + return cls( + removed=rpc_decode_bool(val["removed"]), + log_index=rpc_decode_quantity(val["logIndex"]), + transaction_index=rpc_decode_quantity(val["transactionIndex"]), + transaction_hash=TxHash.rpc_decode(val["transactionHash"]), + block_hash=BlockHash.rpc_decode(val["blockHash"]), + block_number=rpc_decode_quantity(val["blockNumber"]), + address=Address.rpc_decode(val["address"]), + data=rpc_decode_data(val["data"]), + topics=tuple(LogTopic.rpc_decode(topic) for topic in topics), + ) + + class TxReceipt(NamedTuple): """Transaction receipt.""" @@ -496,9 +557,15 @@ class TxReceipt(NamedTuple): succeeded: bool """Whether the transaction was successful.""" + logs: Tuple[LogEntry, ...] + @classmethod def rpc_decode(cls, val: ResponseDict) -> "TxReceipt": contract_address = val["contractAddress"] + logs = val["logs"] + if not isinstance(logs, Iterable): + raise RPCDecodingError(f"`logs` in a tx receipt must be an iterable, got {logs}") + return cls( block_hash=BlockHash.rpc_decode(val["blockHash"]), block_number=rpc_decode_quantity(val["blockNumber"]), @@ -512,67 +579,7 @@ def rpc_decode(cls, val: ResponseDict) -> "TxReceipt": transaction_index=rpc_decode_quantity(val["transactionIndex"]), type_=rpc_decode_quantity(val["type"]), succeeded=(rpc_decode_quantity(val["status"]) == 1), - ) - - -class LogEntry(NamedTuple): - """Log entry metadata.""" - - removed: bool - """ - ``True`` if log was removed, due to a chain reorganization. - ``False`` if it is a valid log. - """ - - address: Address - """ - The contract address from which this log originated. - """ - - data: bytes - """ABI-packed non-indexed arguments of the event.""" - - topics: Tuple[LogTopic, ...] - """ - Values of indexed event fields. - For a named event, the first topic is the event's selector. - """ - - # In the docs of major providers (Infura, Alchemy, Quicknode) it is claimed - # that the following fields can be null if "it is a pending log". - # I could not reproduce such behavior, so for now they're staying non-nullable. - - log_index: int - """Log's position in the block.""" - - transaction_index: int - """Transaction's position in the block.""" - - transaction_hash: TxHash - """Hash of the transactions this log was created from.""" - - block_hash: BlockHash - """Hash of the block where this log was in.""" - - block_number: int - """The block number where this log was.""" - - @classmethod - def rpc_decode(cls, val: ResponseDict) -> "LogEntry": - topics = val["topics"] - if not isinstance(topics, Iterable): - raise RPCDecodingError(f"`topics` in a log entry must be an iterable, got {topics}") - - return cls( - removed=rpc_decode_bool(val["removed"]), - log_index=rpc_decode_quantity(val["logIndex"]), - transaction_index=rpc_decode_quantity(val["transactionIndex"]), - transaction_hash=TxHash.rpc_decode(val["transactionHash"]), - block_hash=BlockHash.rpc_decode(val["blockHash"]), - block_number=rpc_decode_quantity(val["blockNumber"]), - address=Address.rpc_decode(val["address"]), - data=rpc_decode_data(val["data"]), - topics=tuple(LogTopic.rpc_decode(topic) for topic in topics), + logs=tuple(LogEntry.rpc_decode(ResponseDict(entry)) for entry in logs), ) diff --git a/pons/_local_provider.py b/pons/_local_provider.py index 0cd6b6e..03103cb 100644 --- a/pons/_local_provider.py +++ b/pons/_local_provider.py @@ -97,6 +97,8 @@ def make_camel_case(key: str) -> str: def normalize_return_value(value: Normalizable) -> JSON: + if isinstance(value, bool): + return value if isinstance(value, int): return rpc_encode_quantity(value) if isinstance(value, bytes): @@ -234,6 +236,9 @@ def eth_get_transaction_receipt(self, tx_hash: str) -> JSON: result = self._ethereum_tester.get_transaction_receipt(tx_hash) except TransactionNotFound: return None + for entry in result["logs"]: + # returned by regular RPC providers, but not by EthereumTester + entry["removed"] = False return normalize_return_value(result) def eth_estimate_gas(self, tx: Mapping[str, Any], block: str) -> str: diff --git a/tests/test_entities.py b/tests/test_entities.py index 55696ae..8ffe76a 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -162,6 +162,19 @@ def test_decode_tx_receipt(): "to": None, "transactionIndex": "0x18", "type": "0x0", + "logs": [ + { + "address": "0x388c818ca8b9251b393131c08a736a67ccb19297", + "blockHash": "0x0a79eca9f5ca58a1d5d5030a0fabfdd8e815b8b77a9f223f74d59aa39596e1c7", + "blockNumber": "0x11e5883", + "data": "0x00000000000000000000000000000000000000000000000011b6b79503fb875d", + "logIndex": "0x187", + "removed": False, + "topics": ["0x27f12abfe35860a9a927b465bb3d4a9c23c8428174b83f278fe45ed7b4da2662"], + "transactionHash": "0x7114b4da1a6ed391d5d781447ed443733dcf2b508c515b81c17379dea8a3c9af", + "transactionIndex": "0x76", + } + ], } tx_receipt = TxReceipt.rpc_decode(tx_receipt_json) @@ -179,6 +192,10 @@ def test_decode_tx_receipt(): assert tx_receipt.contract_address is None assert tx_receipt.to == address + tx_receipt_json["logs"] = 1 + with pytest.raises(RPCDecodingError, match="`logs` in a tx receipt must be an iterable, got 1"): + TxReceipt.rpc_decode(tx_receipt_json) + def test_encode_decode_quantity(): val = 100 From 95cf03e6bff9df0fbaead822dcf72bbc54ae0ea0 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 10 Feb 2024 12:32:31 -0800 Subject: [PATCH 5/6] Get the default nonce based on the pending block --- docs/changelog.rst | 1 + pons/_client.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index abdc268..1ca9031 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -41,6 +41,7 @@ Fixed - The transaction tip being set larger than the max gas price (which some providers don't like). (PR_64_) - Decoding error when fetching pending transactions. (PR_65_) - Decoding error when fetching pending blocks. (PR_67_) +- Get the default nonce based on the pending block, not the latest one. (PR_68_) .. _PR_51: https://github.com/fjarri/pons/pull/51 diff --git a/pons/_client.py b/pons/_client.py index 62c6cf1..307755b 100644 --- a/pons/_client.py +++ b/pons/_client.py @@ -547,7 +547,7 @@ async def broadcast_transfer( # TODO (#19): implement gas strategies max_gas_price = await self.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) - nonce = await self.eth_get_transaction_count(signer.address, Block.LATEST) + nonce = await self.eth_get_transaction_count(signer.address, Block.PENDING) tx: Dict[str, Union[int, str]] = { "type": 2, # EIP-2930 transaction "chainId": rpc_encode_quantity(chain_id), @@ -613,7 +613,7 @@ async def deploy( # TODO (#19): implement gas strategies max_gas_price = await self.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) - nonce = await self.eth_get_transaction_count(signer.address, Block.LATEST) + nonce = await self.eth_get_transaction_count(signer.address, Block.PENDING) tx: Dict[str, Union[int, str]] = { "type": 2, # EIP-2930 transaction "chainId": rpc_encode_quantity(chain_id), @@ -665,7 +665,7 @@ async def broadcast_transact( # TODO (#19): implement gas strategies max_gas_price = await self.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) - nonce = await self.eth_get_transaction_count(signer.address, Block.LATEST) + nonce = await self.eth_get_transaction_count(signer.address, Block.PENDING) tx: Dict[str, Union[int, str]] = { "type": 2, # EIP-2930 transaction "chainId": rpc_encode_quantity(chain_id), From ffdb7caadfc54a4c179551985fc5436424aa3298 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Sat, 10 Feb 2024 12:49:12 -0800 Subject: [PATCH 6/6] Add `ClientSession.eth_get_logs()` and `eth_get_filter_logs()` --- docs/changelog.rst | 1 + pons/_client.py | 101 +++++++++++++++++++++++++++++----------- pons/_local_provider.py | 25 ++++++++++ tests/test_client.py | 53 +++++++++++++++++++++ tests/test_entities.py | 4 +- 5 files changed, 155 insertions(+), 29 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1ca9031..2e544d0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,6 +31,7 @@ Added - ``eth_getCode`` support (as ``ClientSession.eth_get_code()``). (PR_64_) - ``eth_getStorageAt`` support (as ``ClientSession.eth_get_storage_at()``). (PR_64_) - Support for the ``logs`` field in ``TxReceipt``. (PR_68_) +- ``ClientSession.eth_get_logs()`` and ``eth_get_filter_logs()``. (PR_68_) Fixed diff --git a/pons/_client.py b/pons/_client.py index 307755b..79a8525 100644 --- a/pons/_client.py +++ b/pons/_client.py @@ -739,6 +739,49 @@ async def transact( return results + def _encode_filter_params( + self, + source: Optional[Union[Address, Iterable[Address]]], + event_filter: Optional[EventFilter], + from_block: Union[int, Block], + to_block: Union[int, Block], + ) -> JSON: + params: Dict[str, Any] = { + "fromBlock": rpc_encode_block(from_block), + "toBlock": rpc_encode_block(to_block), + } + if isinstance(source, Address): + params["address"] = source.rpc_encode() + elif source: + params["address"] = [address.rpc_encode() for address in source] + if event_filter: + encoded_topics: List[Optional[List[str]]] = [] + for topic in event_filter.topics: + if topic is None: + encoded_topics.append(None) + else: + encoded_topics.append([elem.rpc_encode() for elem in topic]) + params["topics"] = encoded_topics + return params + + @rpc_call("eth_getLogs") + async def eth_get_logs( + self, + source: Optional[Union[Address, Iterable[Address]]] = None, + event_filter: Optional[EventFilter] = None, + from_block: Union[int, Block] = Block.LATEST, + to_block: Union[int, Block] = Block.LATEST, + ) -> Tuple[LogEntry, ...]: + """Calls the ``eth_getLogs`` RPC method.""" + params = self._encode_filter_params( + source=source, event_filter=event_filter, from_block=from_block, to_block=to_block + ) + result = await self._provider_session.rpc("eth_getLogs", params) + # TODO: this will go away with generalized RPC decoding. + if not isinstance(result, list): + raise InvalidResponse(f"Expected a list as a response, got {type(result).__name__}") + return tuple(LogEntry.rpc_decode(ResponseDict(elem)) for elem in result) + @rpc_call("eth_newBlockFilter") async def eth_new_block_filter(self) -> BlockFilter: """Calls the ``eth_newBlockFilter`` RPC method.""" @@ -764,27 +807,38 @@ async def eth_new_filter( to_block: Union[int, Block] = Block.LATEST, ) -> LogFilter: """Calls the ``eth_newFilter`` RPC method.""" - params: Dict[str, Any] = { - "fromBlock": rpc_encode_block(from_block), - "toBlock": rpc_encode_block(to_block), - } - if isinstance(source, Address): - params["address"] = source.rpc_encode() - elif source: - params["address"] = [address.rpc_encode() for address in source] - if event_filter: - encoded_topics: List[Optional[List[str]]] = [] - for topic in event_filter.topics: - if topic is None: - encoded_topics.append(None) - else: - encoded_topics.append([elem.rpc_encode() for elem in topic]) - params["topics"] = encoded_topics - + params = self._encode_filter_params( + source=source, event_filter=event_filter, from_block=from_block, to_block=to_block + ) result, provider_path = await self._provider_session.rpc_and_pin("eth_newFilter", params) filter_id = LogFilterId.rpc_decode(result) return LogFilter(id_=filter_id, provider_path=provider_path) + def _parse_filter_result( + self, + filter_: Union[BlockFilter, PendingTransactionFilter, LogFilter], + result: JSON, + ) -> Union[Tuple[BlockHash, ...], Tuple[TxHash, ...], Tuple[LogEntry, ...]]: + # TODO: this will go away with generalized RPC decoding. + if not isinstance(result, list): + raise InvalidResponse(f"Expected a list as a response, got {type(result).__name__}") + + if isinstance(filter_, BlockFilter): + return tuple(BlockHash.rpc_decode(elem) for elem in result) + if isinstance(filter_, PendingTransactionFilter): + return tuple(TxHash.rpc_decode(elem) for elem in result) + return tuple(LogEntry.rpc_decode(ResponseDict(elem)) for elem in result) + + @rpc_call("eth_getFilterLogs") + async def eth_get_filter_logs( + self, filter_: Union[BlockFilter, PendingTransactionFilter, LogFilter] + ) -> Union[Tuple[BlockHash, ...], Tuple[TxHash, ...], Tuple[LogEntry, ...]]: + """Calls the ``eth_getFilterLogs`` RPC method.""" + result = await self._provider_session.rpc_at_pin( + filter_.provider_path, "eth_getFilterLogs", filter_.id_.rpc_encode() + ) + return self._parse_filter_result(filter_, result) + @rpc_call("eth_getFilterChanges") async def eth_get_filter_changes( self, filter_: Union[BlockFilter, PendingTransactionFilter, LogFilter] @@ -794,19 +848,10 @@ async def eth_get_filter_changes( Depending on what ``filter_`` was, returns a tuple of corresponding results. """ # TODO: split into separate functions with specific return types? - results = await self._provider_session.rpc_at_pin( + result = await self._provider_session.rpc_at_pin( filter_.provider_path, "eth_getFilterChanges", filter_.id_.rpc_encode() ) - - # TODO: this will go away with generalized RPC decoding. - if not isinstance(results, list): - raise InvalidResponse(f"Expected a list as a response, got {type(results).__name__}") - - if isinstance(filter_, BlockFilter): - return tuple(BlockHash.rpc_decode(elem) for elem in results) - if isinstance(filter_, PendingTransactionFilter): - return tuple(TxHash.rpc_decode(elem) for elem in results) - return tuple(LogEntry.rpc_decode(ResponseDict(elem)) for elem in results) + return self._parse_filter_result(filter_, result) async def iter_blocks(self, poll_interval: int = 1) -> AsyncIterator[BlockHash]: """Yields hashes of new blocks being mined.""" diff --git a/pons/_local_provider.py b/pons/_local_provider.py index 03103cb..51136d7 100644 --- a/pons/_local_provider.py +++ b/pons/_local_provider.py @@ -183,6 +183,8 @@ def rpc(self, method: str, *args: Any) -> JSON: eth_newPendingTransactionFilter=self.eth_new_pending_transaction_filter, eth_newFilter=self.eth_new_filter, eth_getFilterChanges=self.eth_get_filter_changes, + eth_getLogs=self.eth_get_logs, + eth_getFilterLogs=self.eth_get_filter_logs, ) if method not in dispatch: raise RPCError.method_not_found(method) @@ -346,6 +348,29 @@ def eth_get_filter_changes(self, filter_id: str) -> JSON: result["removed"] = False return cast(JSON, results) + def eth_get_logs(self, params: Mapping[str, Any]) -> JSON: + address = params.get("address", None) + topics = params.get("topics", None) + results = self._ethereum_tester.get_logs( + from_block=rpc_decode_block(params["fromBlock"]), + to_block=rpc_decode_block(params["toBlock"]), + address=address, + topics=topics, + ) + results = normalize_return_value(results) + for result in results: + # returned by regular RPC providers, but not by EthereumTester + result["removed"] = False + return cast(JSON, results) + + def eth_get_filter_logs(self, filter_id: str) -> JSON: + results = self._ethereum_tester.get_all_filter_logs(rpc_decode_quantity(filter_id)) + results = normalize_return_value(results) + for result in results: + # returned by regular RPC providers, but not by EthereumTester + result["removed"] = False + return cast(JSON, results) + @asynccontextmanager async def session(self) -> AsyncIterator["LocalProviderSession"]: yield LocalProviderSession(self) diff --git a/tests/test_client.py b/tests/test_client.py index d29b9c8..45dafa9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -620,6 +620,59 @@ async def test_pending_transaction_filter(local_provider, session, root_signer, 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 + ) + + # Test an invalid response + + monkeypatch.setattr(local_provider, "eth_get_logs", lambda _filter_id: {"foo": 1}) + + with pytest.raises( + BadResponseFormat, match=r"eth_getLogs: Expected a list as a response, got dict" + ): + 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)) diff --git a/tests/test_entities.py b/tests/test_entities.py index 8ffe76a..c1097b4 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -171,7 +171,9 @@ def test_decode_tx_receipt(): "logIndex": "0x187", "removed": False, "topics": ["0x27f12abfe35860a9a927b465bb3d4a9c23c8428174b83f278fe45ed7b4da2662"], - "transactionHash": "0x7114b4da1a6ed391d5d781447ed443733dcf2b508c515b81c17379dea8a3c9af", + "transactionHash": ( + "0x7114b4da1a6ed391d5d781447ed443733dcf2b508c515b81c17379dea8a3c9af" + ), "transactionIndex": "0x76", } ],