From 7f0bd558db0c32ed50813877a7e2ddf525be15b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ux=C3=ADo?= Date: Wed, 8 Mar 2023 12:01:40 +0100 Subject: [PATCH] Fix duplicated webhook calls (#1354) * Add processed cache to ERC20/721 events indexer * Add processed cache to Safe events indexer (not really needed as it doesn't use `bulk_create` but still saves processing an event and some calls to the database) * Remove not needed methods from ERC20 indexer * Add processed cache to InternalTxIndexer * Refactor tests * When there's a reorg reset the indexers status * Remove deprecated docs --- .../history/indexers/erc20_events_indexer.py | 69 +- .../history/indexers/events_indexer.py | 51 +- .../history/indexers/internal_tx_indexer.py | 66 +- safe_transaction_service/history/models.py | 10 +- .../history/services/reorg_service.py | 18 + .../tests/mocks/mocks_erc20_events_indexer.py | 34 + .../tests/mocks/mocks_safe_events_indexer.py | 605 ++++++++++++++++++ .../tests/test_erc20_events_indexer.py | 72 +-- .../history/tests/test_internal_tx_indexer.py | 57 +- .../history/tests/test_safe_events_indexer.py | 49 +- safe_transaction_service/utils/utils.py | 18 + 11 files changed, 940 insertions(+), 109 deletions(-) create mode 100644 safe_transaction_service/history/tests/mocks/mocks_erc20_events_indexer.py create mode 100644 safe_transaction_service/history/tests/mocks/mocks_safe_events_indexer.py diff --git a/safe_transaction_service/history/indexers/erc20_events_indexer.py b/safe_transaction_service/history/indexers/erc20_events_indexer.py index 8d607b50e..b33fdf491 100644 --- a/safe_transaction_service/history/indexers/erc20_events_indexer.py +++ b/safe_transaction_service/history/indexers/erc20_events_indexer.py @@ -1,22 +1,16 @@ -import operator from collections import OrderedDict from logging import getLogger from typing import Iterator, List, Optional, Sequence from django.db.models import QuerySet -from cache_memoize import cache_memoize -from cachetools import cachedmethod -from eth_abi.exceptions import DecodingError from eth_typing import ChecksumAddress from web3.contract import ContractEvent -from web3.exceptions import BadFunctionCallOutput from web3.types import EventData, LogReceipt from gnosis.eth import EthereumClient -from safe_transaction_service.tokens.models import Token - +from ...utils.utils import FixedSizeDict from ..models import ( ERC20Transfer, ERC721Transfer, @@ -47,12 +41,22 @@ def del_singleton(cls): class Erc20EventsIndexer(EventsIndexer): - _cache_is_erc20 = {} - """ - Indexes ERC20 and ERC721 `Transfer` Event (as ERC721 has the same topic) + Indexes `ERC20` and `ERC721` `Transfer` events. + + ERC20 Transfer Event: `Transfer(address indexed from, address indexed to, uint256 value)` + ERC721 Transfer Event: `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)` + + As `event topic` is the same both events can be indexed together, and then tell + apart based on the `indexed` part as `indexed` elements are stored in a different way in the + `ethereum tx receipt`. """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._processed_element_cache = FixedSizeDict(maxlen=40_000) # Around 3MiB + @property def contract_events(self) -> List[ContractEvent]: """ @@ -107,36 +111,14 @@ def _do_node_query( or transfer_event["args"]["from"] in addresses_set ] - @cachedmethod(cache=operator.attrgetter("_cache_is_erc20")) - @cache_memoize(60 * 60 * 24, prefix="erc20-events-indexer-is-erc20") # 1 day - def _is_erc20(self, token_address: str) -> bool: - try: - token = Token.objects.get(address=token_address) - return token.is_erc20() - except Token.DoesNotExist: - try: - decimals = self.ethereum_client.erc20.get_decimals(token_address) - return decimals is not None - except (ValueError, BadFunctionCallOutput, DecodingError): - return False - - def _process_decoded_element(self, event: EventData) -> EventData: - """ - :param event: Be careful, it will be modified instead of copied - :return: The same event if it's a ERC20/ERC721. Tries to tell apart if it's not defined (`unknown` instead - of `value` or `tokenId`) + def _process_decoded_element(self, decoded_element: EventData) -> None: """ - event_args = event["args"] - if "unknown" in event_args: # Not standard event - event_args["value"] = event_args.pop("unknown") + Not used as `process_elements` is redefined using custom processors - if self._is_erc20(event["address"]): - if "tokenId" in event_args: - event_args["value"] = event_args.pop("tokenId") - else: - if "value" in event_args: - event_args["tokenId"] = event_args.pop("value") - return event + :param decoded_element: + :return: + """ + pass def events_to_erc20_transfer( self, log_receipts: Sequence[EventData] @@ -176,11 +158,18 @@ def process_elements( logger.debug("End prefetching and storing of ethereum txs") logger.debug("Storing TokenTransfer objects") + not_processed_log_receipts = [ + log_receipt + for log_receipt in log_receipts + if self.mark_as_processed(log_receipt) + ] result_erc20 = ERC20Transfer.objects.bulk_create_from_generator( - self.events_to_erc20_transfer(log_receipts), ignore_conflicts=True + self.events_to_erc20_transfer(not_processed_log_receipts), + ignore_conflicts=True, ) result_erc721 = ERC721Transfer.objects.bulk_create_from_generator( - self.events_to_erc721_transfer(log_receipts), ignore_conflicts=True + self.events_to_erc721_transfer(not_processed_log_receipts), + ignore_conflicts=True, ) logger.debug("Stored TokenTransfer objects") return range( diff --git a/safe_transaction_service/history/indexers/events_indexer.py b/safe_transaction_service/history/indexers/events_indexer.py index 15348ae54..228911240 100644 --- a/safe_transaction_service/history/indexers/events_indexer.py +++ b/safe_transaction_service/history/indexers/events_indexer.py @@ -14,7 +14,7 @@ from web3.exceptions import LogTopicError from web3.types import EventData, FilterParams, LogReceipt -from safe_transaction_service.utils.utils import chunks +from safe_transaction_service.utils.utils import FixedSizeDict, chunks from .ethereum_indexer import EthereumIndexer, FindRelevantElementsException @@ -51,9 +51,43 @@ def __init__(self, *args, **kwargs): # Number of concurrent requests to `getLogs` self.get_logs_concurrency = settings.ETH_EVENTS_GET_LOGS_CONCURRENCY + self._processed_element_cache = FixedSizeDict(maxlen=40_000) # Around 3MiB super().__init__(*args, **kwargs) + def mark_as_processed(self, log_receipt: LogReceipt) -> bool: + """ + Mark event as processed by the indexer + + :param log_receipt: + :return: `True` if `event` was marked as processed, `False` if it was already processed + """ + + # Calculate id, collision should be almost impossible + # Add blockHash to prevent reorg issues + block_hash = HexBytes(log_receipt["blockHash"]) + tx_hash = HexBytes(log_receipt["transactionHash"]) + log_index = log_receipt["logIndex"] + event_id = block_hash + tx_hash + HexBytes(log_index) + + if event_id in self._processed_element_cache: + logger.debug( + "Event with tx-hash=%s log-index=%d on block=%s was already processed", + tx_hash.hex(), + log_index, + block_hash.hex(), + ) + return False + else: + logger.debug( + "Marking event with tx-hash=%s log-index=%d on block=%s as processed", + tx_hash.hex(), + log_index, + block_hash.hex(), + ) + self._processed_element_cache[event_id] = None + return True + @property @abstractmethod def contract_events(self) -> List[ContractEvent]: @@ -229,9 +263,17 @@ def process_elements(self, log_receipts: Sequence[LogReceipt]) -> List[Any]: if not log_receipts: return [] - decoded_elements: List[EventData] = self.decode_elements(log_receipts) + # Ignore already processed events + not_processed_log_receipts = [ + log_receipt + for log_receipt in log_receipts + if self.mark_as_processed(log_receipt) + ] + decoded_elements: List[EventData] = self.decode_elements( + not_processed_log_receipts + ) tx_hashes = OrderedDict.fromkeys( - [event["transactionHash"] for event in log_receipts] + [event["transactionHash"] for event in not_processed_log_receipts] ).keys() logger.debug("Prefetching and storing %d ethereum txs", len(tx_hashes)) self.index_service.txs_create_or_update_from_tx_hashes(tx_hashes) @@ -239,8 +281,7 @@ def process_elements(self, log_receipts: Sequence[LogReceipt]) -> List[Any]: logger.debug("Processing %d decoded events", len(decoded_elements)) processed_elements = [] for decoded_element in decoded_elements: - processed_element = self._process_decoded_element(decoded_element) - if processed_element: + if processed_element := self._process_decoded_element(decoded_element): processed_elements.append(processed_element) logger.debug("End processing %d decoded events", len(decoded_elements)) return processed_elements diff --git a/safe_transaction_service/history/indexers/internal_tx_indexer.py b/safe_transaction_service/history/indexers/internal_tx_indexer.py index 1032a50a2..cccff83f5 100644 --- a/safe_transaction_service/history/indexers/internal_tx_indexer.py +++ b/safe_transaction_service/history/indexers/internal_tx_indexer.py @@ -6,6 +6,7 @@ from django.db import transaction from eth_typing import HexStr +from hexbytes import HexBytes from web3.types import ParityBlockTrace, ParityFilterTrace from gnosis.eth import EthereumClient @@ -14,7 +15,7 @@ CannotDecode, get_safe_tx_decoder, ) -from safe_transaction_service.utils.utils import chunks +from safe_transaction_service.utils.utils import FixedSizeDict, chunks from ..models import InternalTx, InternalTxDecoded, MonitoredAddress, SafeMasterCopy from .ethereum_indexer import EthereumIndexer, FindRelevantElementsException @@ -56,6 +57,38 @@ def __init__(self, *args, **kwargs): 10 # Use `trace_block` for last `number_trace_blocks` blocks indexing ) self.tx_decoder = get_safe_tx_decoder() + self._processed_element_cache = FixedSizeDict(maxlen=40_000) # Around 3MiB + + def mark_as_processed( + self, tx_hash: HexBytes, block_hash: Optional[HexBytes] + ) -> bool: + """ + Mark a `tx_hash` as processed by the indexer + + :param tx_hash: + :param block_hash: + :return: `True` if `tx_hash` was marked as processed, `False` if it was already processed + """ + + tx_hash = HexBytes(tx_hash) + block_hash = HexBytes(block_hash or 0) + tx_id = tx_hash + block_hash + + if tx_id in self._processed_element_cache: + logger.debug( + "Tx with tx-hash=%s on block=%s was already processed", + tx_hash.hex(), + block_hash.hex(), + ) + return False + else: + logger.debug( + "Marking tx with tx-hash=%s on block=%s as processed", + tx_hash.hex(), + block_hash.hex(), + ) + self._processed_element_cache[tx_id] = None + return True @property def database_field(self): @@ -251,21 +284,44 @@ def trace_transactions( def process_elements( self, tx_hash_with_traces: OrderedDict[HexStr, Optional[ParityFilterTrace]] ) -> List[InternalTx]: + """ + :param tx_hash_with_traces: + :return: Inserted `InternalTx` objects + """ # Prefetch ethereum txs if not tx_hash_with_traces: return [] + # Copy as we might modify it + tx_hash_with_traces = dict(tx_hash_with_traces) + logger.debug( "Prefetching and storing %d ethereum txs", len(tx_hash_with_traces) ) - tx_hashes = list(tx_hash_with_traces.keys()) + + tx_hashes = [] + tx_hashes_missing_traces = [] + for tx_hash in list(tx_hash_with_traces.keys()): + # Check if transactions have already been processed + # Provide block_hash if available as a mean to prevent reorgs + block_hash = ( + tx_hash_with_traces[tx_hash][0]["blockHash"] + if tx_hash_with_traces[tx_hash] + else None + ) + if self.mark_as_processed(tx_hash, block_hash): + tx_hashes.append(tx_hash) + # Traces can be already populated if using `trace_block`, but with `trace_filter` + # some traces will be missing and `trace_transaction` needs to be called + if not tx_hash_with_traces[tx_hash]: + tx_hashes_missing_traces.append(tx_hash) + else: + del tx_hash_with_traces[tx_hash] + ethereum_txs = self.index_service.txs_create_or_update_from_tx_hashes(tx_hashes) logger.debug("End prefetching and storing of ethereum txs") logger.debug("Prefetching of traces(internal txs)") - tx_hashes_missing_traces = [ - tx_hash for tx_hash, trace in tx_hash_with_traces.items() if not trace - ] missing_traces_lists = self.trace_transactions( tx_hashes_missing_traces, batch_size=self.trace_txs_batch_size ) diff --git a/safe_transaction_service/history/models.py b/safe_transaction_service/history/models.py index 55ecdc050..f55282ec7 100644 --- a/safe_transaction_service/history/models.py +++ b/safe_transaction_service/history/models.py @@ -7,7 +7,7 @@ from typing import ( Any, Dict, - Iterable, + Iterator, List, Optional, Sequence, @@ -135,19 +135,23 @@ def bulk_create( return result def bulk_create_from_generator( - self, objs: Iterable[Any], batch_size: int = 100, ignore_conflicts: bool = False + self, objs: Iterator[Any], batch_size: int = 100, ignore_conflicts: bool = False ) -> int: """ Implementation in Django is not ok, as it will do `objs = list(objs)`. If objects come from a generator they will be brought to RAM. This approach is more friendly + :return: Count of inserted elements """ assert batch_size is not None and batch_size > 0 + iterator = iter( + objs + ) # Make sure we are not slicing the same elements if a sequence is provided total = 0 while True: if inserted := len( self.bulk_create( - islice(objs, batch_size), ignore_conflicts=ignore_conflicts + islice(iterator, batch_size), ignore_conflicts=ignore_conflicts ) ): total += inserted diff --git a/safe_transaction_service/history/services/reorg_service.py b/safe_transaction_service/history/services/reorg_service.py index bfba68bdf..71740a93c 100644 --- a/safe_transaction_service/history/services/reorg_service.py +++ b/safe_transaction_service/history/services/reorg_service.py @@ -7,6 +7,12 @@ from gnosis.eth import EthereumClient, EthereumClientProvider +from ..indexers import ( + Erc20EventsIndexerProvider, + InternalTxIndexerProvider, + ProxyFactoryIndexerProvider, + SafeEventsIndexerProvider, +) from ..models import EthereumBlock, IndexingStatus, ProxyFactory, SafeMasterCopy logger = logging.getLogger(__name__) @@ -59,6 +65,14 @@ def __init__( ), ] + # Indexers to reset + self.indexer_providers = [ + Erc20EventsIndexerProvider, + InternalTxIndexerProvider, + ProxyFactoryIndexerProvider, + SafeEventsIndexerProvider, + ] + def check_reorgs(self) -> Optional[int]: """ :return: Number of the oldest block with reorg detected. `None` if not reorg found @@ -95,6 +109,10 @@ def reset_all_to_block(self, block_number: int) -> int: for reorg_function in self.reorg_functions: updated += reorg_function(block_number) + # Reset indexer status and caches + for indexer_provider in self.indexer_providers: + indexer_provider.del_singleton() + return updated @transaction.atomic diff --git a/safe_transaction_service/history/tests/mocks/mocks_erc20_events_indexer.py b/safe_transaction_service/history/tests/mocks/mocks_erc20_events_indexer.py new file mode 100644 index 000000000..ccb5c7894 --- /dev/null +++ b/safe_transaction_service/history/tests/mocks/mocks_erc20_events_indexer.py @@ -0,0 +1,34 @@ +from hexbytes import HexBytes + +log_receipt_mock = [ + { + "address": "0xD84dbd5138D2297959Ae56602Bd5B2A035bb3F59", + "blockHash": HexBytes( + "0xe630ebf8c8ff2397896f23de27fd6e9f280d4ede613acbf788d545cc0c5194e8" + ), + "blockNumber": 6, + "data": "0x000000000000000000000000000000000000000000000000000000000000000a", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + ), + HexBytes( + "0x00000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b" + ), + HexBytes( + "0x0000000000000000000000006e5b7093ac36ea61da02fd1cceecf56fd6626d48" + ), + ], + "transactionHash": HexBytes( + "0x53a869a24855dcae97e6cea9069eb7a2e57c45a3538081947a1af7a7da38d627" + ), + "transactionIndex": 0, + "args": { + "from": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b", + "to": "0x6e5B7093aC36EA61da02Fd1CcEeCF56fD6626D48", + "value": 10, + }, + } +] diff --git a/safe_transaction_service/history/tests/mocks/mocks_safe_events_indexer.py b/safe_transaction_service/history/tests/mocks/mocks_safe_events_indexer.py new file mode 100644 index 000000000..3861f6915 --- /dev/null +++ b/safe_transaction_service/history/tests/mocks/mocks_safe_events_indexer.py @@ -0,0 +1,605 @@ +from hexbytes import HexBytes +from web3.datastructures import AttributeDict + +safe_events_mock = [ + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xdfa4d85183c64488211b6a270a9b1231463d014538fe61586fe83a650ca62576" + ), + "blockNumber": 76, + "data": "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x141df868a6331af528e38c83b7aa03edc19be66e37ae67f9285bf4f8e3c6a1a8" + ), + HexBytes( + "0x000000000000000000000000a21e2615ed32ce9ddfc53a1b0ccfe689e9152f25" + ), + ], + "transactionHash": HexBytes( + "0x2b4d9886e32fdd5b05072b592c7b5d2e50020282cd7d234a2054e71458887c36" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0xA21E2615ED32CE9DdFc53A1B0ccFE689e9152f25", + "blockHash": HexBytes( + "0xdfa4d85183c64488211b6a270a9b1231463d014538fe61586fe83a650ca62576" + ), + "blockNumber": 76, + "data": "0x0000000000000000000000000059c65c3d2325d77e9288e022d24d3972b1799d0000000000000000000000009c82244a30cd4b2591f9a8e8ad0dab0fa643ee34", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0x4f51faf6c4561ff95f067657e43439f0f856d97c04d9ec9070a6199ad418e235" + ) + ], + "transactionHash": HexBytes( + "0x2b4d9886e32fdd5b05072b592c7b5d2e50020282cd7d234a2054e71458887c36" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x2ccafdf23032b07507a0e9e8bc18d6e44f7963878147975aa30128f87db43dc5" + ), + "blockNumber": 77, + "data": "0x0000000000000000000000000059c65c3d2325d77e9288e022d24d3972b1799d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000440d582f13000000000000000000000000daa47edf1ad3e68980be7da6bc9d21e14ce49ff70000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041e415afe9701106948ee3b4a092f08dd7533d716e875668d0c880567422c233f15dcf94f57cbc2fb6c10a7c1a33cac9ededa09621d21fea3103b4e2f69bffcdd51c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x66753cd2356569ee081232e3be8909b950e0a76c1f8460c3a5e3c2be32b11bed" + ) + ], + "transactionHash": HexBytes( + "0x17e4424aaf4cdc16268051cbd2c168e9a19056a18161b6a45241fb5beb4044a2" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x2ccafdf23032b07507a0e9e8bc18d6e44f7963878147975aa30128f87db43dc5" + ), + "blockNumber": 77, + "data": "0x000000000000000000000000daa47edf1ad3e68980be7da6bc9d21e14ce49ff7", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0x9465fa0c962cc76958e6373a993326400c1c94f8be2fe3a952adfa7f60b2ea26" + ) + ], + "transactionHash": HexBytes( + "0x17e4424aaf4cdc16268051cbd2c168e9a19056a18161b6a45241fb5beb4044a2" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x2ccafdf23032b07507a0e9e8bc18d6e44f7963878147975aa30128f87db43dc5" + ), + "blockNumber": 77, + "data": "0x02cb808a8744be87acce11db5826431719294e049ea9bdd7bbdbdaa33dd489970000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 2, + "removed": False, + "topics": [ + HexBytes( + "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e" + ) + ], + "transactionHash": HexBytes( + "0x17e4424aaf4cdc16268051cbd2c168e9a19056a18161b6a45241fb5beb4044a2" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x9dae812e6792049fadbbb152c82e9f7b4fb59251d04f39f7d8f67ec9133e1ef6" + ), + "blockNumber": 78, + "data": "0x0000000000000000000000000059c65c3d2325d77e9288e022d24d3972b1799d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000024694e80c30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041144f4835f26ef8e9dfa63287a18affef8a6c9316ef82276bdacdc1670f54e08f689d81c0fa75a87c090517fc5986338f4106cef112859d1a96f3599045e985831b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x66753cd2356569ee081232e3be8909b950e0a76c1f8460c3a5e3c2be32b11bed" + ) + ], + "transactionHash": HexBytes( + "0xd62d615a9450c71ec33ccedc3a938b768e9d1768d2b6275251cb86bbc54d85d4" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x9dae812e6792049fadbbb152c82e9f7b4fb59251d04f39f7d8f67ec9133e1ef6" + ), + "blockNumber": 78, + "data": "0x0000000000000000000000000000000000000000000000000000000000000002", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0x610f7ff2b304ae8903c3de74c60c6ab1f7d6226b3f52c5161905bb5ad4039c93" + ) + ], + "transactionHash": HexBytes( + "0xd62d615a9450c71ec33ccedc3a938b768e9d1768d2b6275251cb86bbc54d85d4" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x9dae812e6792049fadbbb152c82e9f7b4fb59251d04f39f7d8f67ec9133e1ef6" + ), + "blockNumber": 78, + "data": "0xf41c0b7215bb201b5a0e698e50c4e56d1cb51b8d2259eeeff25761c2abf00c260000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 2, + "removed": False, + "topics": [ + HexBytes( + "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e" + ) + ], + "transactionHash": HexBytes( + "0xd62d615a9450c71ec33ccedc3a938b768e9d1768d2b6275251cb86bbc54d85d4" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xa4a7be9cf00a367bef20f008bf7131f9c8bea3520c6c1efce4e85f27c7c4396b" + ), + "blockNumber": 79, + "data": "0x0000000000000000000000000059c65c3d2325d77e9288e022d24d3972b1799d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000064f8dc5dd90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000daa47edf1ad3e68980be7da6bc9d21e14ce49ff7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008234b70dc3bf9189e0614d95c47513efd3c902212d9ebeef8d2edd8652c3a2ce6c762821c44914eac76b8fc7767266cfcd5295fa665be0b60baaf0d481e1c5f4741c617bc2a2ad6800a5cd6733cee20bc0b9a450732d744229e678a97f5c54a4344729c50a73d99332b4776442bf313be620ada4aa71fd15c06c39843417a4a137731c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000200000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b0000000000000000000000000000000000000000000000000000000000000002", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x66753cd2356569ee081232e3be8909b950e0a76c1f8460c3a5e3c2be32b11bed" + ) + ], + "transactionHash": HexBytes( + "0x1d600f00f939bc8fb5ac53fe57e8cdc227e7472030bd5633d7e5e00d8a9333d3" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xa4a7be9cf00a367bef20f008bf7131f9c8bea3520c6c1efce4e85f27c7c4396b" + ), + "blockNumber": 79, + "data": "0x000000000000000000000000daa47edf1ad3e68980be7da6bc9d21e14ce49ff7", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0xf8d49fc529812e9a7c5c50e69c20f0dccc0db8fa95c98bc58cc9a4f1c1299eaf" + ) + ], + "transactionHash": HexBytes( + "0x1d600f00f939bc8fb5ac53fe57e8cdc227e7472030bd5633d7e5e00d8a9333d3" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xa4a7be9cf00a367bef20f008bf7131f9c8bea3520c6c1efce4e85f27c7c4396b" + ), + "blockNumber": 79, + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 2, + "removed": False, + "topics": [ + HexBytes( + "0x610f7ff2b304ae8903c3de74c60c6ab1f7d6226b3f52c5161905bb5ad4039c93" + ) + ], + "transactionHash": HexBytes( + "0x1d600f00f939bc8fb5ac53fe57e8cdc227e7472030bd5633d7e5e00d8a9333d3" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xa4a7be9cf00a367bef20f008bf7131f9c8bea3520c6c1efce4e85f27c7c4396b" + ), + "blockNumber": 79, + "data": "0x79178f56dd6689ad728921814cca7ab5d98fc1d3a68558e3500ac90e005f115b0000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 3, + "removed": False, + "topics": [ + HexBytes( + "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e" + ) + ], + "transactionHash": HexBytes( + "0x1d600f00f939bc8fb5ac53fe57e8cdc227e7472030bd5633d7e5e00d8a9333d3" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x487334a9dbf61d474460e4b1e47e3393b6abb2335f803fafed3fff9f7b4b6a22" + ), + "blockNumber": 80, + "data": "0x0000000000000000000000000059c65c3d2325d77e9288e022d24d3972b1799d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000024610b59250000000000000000000000007af34435997491425cd9fb0edc7537bc32d2075900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004181c82f1897b469fa5f72df49c74cffb952eaee5ddf8342e191d1e5b7c7105c40587131b3f5cf33765f77d2d13dc27bbb2f33ac1337f7c4c977fd8a577deb5a1b1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000300000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x66753cd2356569ee081232e3be8909b950e0a76c1f8460c3a5e3c2be32b11bed" + ) + ], + "transactionHash": HexBytes( + "0xd3b3a40764a015e0af413ef9321bb79595a199415a6342548594801fb1d20f07" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x487334a9dbf61d474460e4b1e47e3393b6abb2335f803fafed3fff9f7b4b6a22" + ), + "blockNumber": 80, + "data": "0x0000000000000000000000007af34435997491425cd9fb0edc7537bc32d20759", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0xecdf3a3effea5783a3c4c2140e677577666428d44ed9d474a0b3a4c9943f8440" + ) + ], + "transactionHash": HexBytes( + "0xd3b3a40764a015e0af413ef9321bb79595a199415a6342548594801fb1d20f07" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x487334a9dbf61d474460e4b1e47e3393b6abb2335f803fafed3fff9f7b4b6a22" + ), + "blockNumber": 80, + "data": "0x4e91973bd128ce787583c3994e7b59c98c15576803ed0eb70312a1108679573f0000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 2, + "removed": False, + "topics": [ + HexBytes( + "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e" + ) + ], + "transactionHash": HexBytes( + "0xd3b3a40764a015e0af413ef9321bb79595a199415a6342548594801fb1d20f07" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xeb7226869a0e79450cfa5a317c59a17c36aea98f95626813d7083b46a143d399" + ), + "blockNumber": 81, + "data": "0x00000000000000000000000000000000000000000000000000000000000004e8", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x3d0ce9bfc3ed7d6862dbb28b2dea94561fe714a1b4d019aa8af39730d1ad7c3d" + ), + HexBytes( + "0x00000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b" + ), + ], + "transactionHash": HexBytes( + "0xfa626ef1b2e4c337a3735a3eeb0abc8a7e2e1dea38422d6d81031b02a828fc35" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x31da4f408aa6aa0cc96b41af28e4e47e775a1ef4df4e5aad7d8f95a930f22afc" + ), + "blockNumber": 82, + "data": "0x0000000000000000000000000059c65c3d2325d77e9288e022d24d3972b1799d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000024f08a0323000000000000000000000000732df3efb1a8613ba4bd25adeacb5d51fecf29230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413f9e59848fc29f28f137bff54d41ebe6d21f92e5c237e23c1c597a16ca28e074132a694adbd3fec8f6797d6a902c72b035e61f97ea0aacbb07fdc51c2d742c041b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000400000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x66753cd2356569ee081232e3be8909b950e0a76c1f8460c3a5e3c2be32b11bed" + ) + ], + "transactionHash": HexBytes( + "0x94f677fb4bfdef01664efb441033e254794f7e83ab693098ad3bdea7c4e5493c" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x31da4f408aa6aa0cc96b41af28e4e47e775a1ef4df4e5aad7d8f95a930f22afc" + ), + "blockNumber": 82, + "data": "0x000000000000000000000000732df3efb1a8613ba4bd25adeacb5d51fecf2923", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0x5ac6c46c93c8d0e53714ba3b53db3e7c046da994313d7ed0d192028bc7c228b0" + ) + ], + "transactionHash": HexBytes( + "0x94f677fb4bfdef01664efb441033e254794f7e83ab693098ad3bdea7c4e5493c" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x31da4f408aa6aa0cc96b41af28e4e47e775a1ef4df4e5aad7d8f95a930f22afc" + ), + "blockNumber": 82, + "data": "0x125c1b0ce7064d57be212deb5c530b21f5cbb210bac4f3759f0b6819d3fdc0200000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 2, + "removed": False, + "topics": [ + HexBytes( + "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e" + ) + ], + "transactionHash": HexBytes( + "0x94f677fb4bfdef01664efb441033e254794f7e83ab693098ad3bdea7c4e5493c" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x212a0881a92e31b97d121f514e78ca3732d99049f2e2613d40541dc4b235f835" + ), + "blockNumber": 83, + "data": "0x0000000000000000000000000059c65c3d2325d77e9288e022d24d3972b1799d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000044e009cfde00000000000000000000000000000000000000000000000000000000000000010000000000000000000000007af34435997491425cd9fb0edc7537bc32d207590000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000416f7a8ee6b13b486c11b4210ad2075423cba6a0b998e8689686d8cd717fd7b5934b4f2af76298f3cd579d8b159b44125d050b37b1b1c8ea5bd7884c389c698d681b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000500000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x66753cd2356569ee081232e3be8909b950e0a76c1f8460c3a5e3c2be32b11bed" + ) + ], + "transactionHash": HexBytes( + "0x7969fff94393671a0645a4c2b93ce97afc87c03040c0ac943a92cd4ff85e56ed" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x212a0881a92e31b97d121f514e78ca3732d99049f2e2613d40541dc4b235f835" + ), + "blockNumber": 83, + "data": "0x0000000000000000000000007af34435997491425cd9fb0edc7537bc32d20759", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0xaab4fa2b463f581b2b32cb3b7e3b704b9ce37cc209b5fb4d77e593ace4054276" + ) + ], + "transactionHash": HexBytes( + "0x7969fff94393671a0645a4c2b93ce97afc87c03040c0ac943a92cd4ff85e56ed" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x212a0881a92e31b97d121f514e78ca3732d99049f2e2613d40541dc4b235f835" + ), + "blockNumber": 83, + "data": "0x1e96ba01790cfd2cbfe5188d1e2c18e7fa9cff63b946029e654379a0346e3d270000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 2, + "removed": False, + "topics": [ + HexBytes( + "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e" + ) + ], + "transactionHash": HexBytes( + "0x7969fff94393671a0645a4c2b93ce97afc87c03040c0ac943a92cd4ff85e56ed" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x75baa2741d1f4056812b7e045ca0df39b7633108d2b86a55965631dc3f7698d3" + ), + "blockNumber": 84, + "data": "0x", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0xf2a0eb156472d1440255b0d7c1e19cc07115d1051fe605b0dce69acfec884d9c" + ), + HexBytes( + "0xc946c1439c69265f7b2b6a4097417fd84796819b3321bab448d13b050dd86d66" + ), + HexBytes( + "0x00000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b" + ), + ], + "transactionHash": HexBytes( + "0xb916e35e13e26ce46c2de4b8beadc992df07678ee98cf3915bf95a999048df8c" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x8ceae137cdecd2007aa0e755af5327906ea782c4f4d8d8c2f851dbdab39cc1f7" + ), + "blockNumber": 85, + "data": "0x0000000000000000000000006b86fef7bd4923ea82def83698150ddec0365390000000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000415ab80cd9f96372a23a533f9216e9e3c04439732699a732ce245f42ea2884ec2c1b6fa45df9649975a8e5707e34cd62e8b29c1d56c6d999332d381052444ad6c71c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000600000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x66753cd2356569ee081232e3be8909b950e0a76c1f8460c3a5e3c2be32b11bed" + ) + ], + "transactionHash": HexBytes( + "0xb547c605de4616a4b4a16c8cce7f89642b3f5bd1552d0e08054268bea871cb7d" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0x8ceae137cdecd2007aa0e755af5327906ea782c4f4d8d8c2f851dbdab39cc1f7" + ), + "blockNumber": 85, + "data": "0xd134f631f147922354d796615ea938d55a57d1acd896b8b48f76e865e6b7208c0000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e" + ) + ], + "transactionHash": HexBytes( + "0xb547c605de4616a4b4a16c8cce7f89642b3f5bd1552d0e08054268bea871cb7d" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xde160b1ae8e89ab7fc20b61cc5d8ee8944fdd29826470f301b89cbaf14a6aa1f" + ), + "blockNumber": 86, + "data": "0x0000000000000000000000000059c65c3d2325d77e9288e022d24d3972b1799d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000024e19a9dd9000000000000000000000000cb03e30c7bd004a5eca1d88b55409033b825eb910000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414b89c9d44d5e7428fdec84f49d248d3116c2ba7f6a0b5edc4e3d020803e68b1f6257f67b5fcf4eacea6bb0f77324617315619ae9cae6db1f9dd51309b4ac42451c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000700000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x66753cd2356569ee081232e3be8909b950e0a76c1f8460c3a5e3c2be32b11bed" + ) + ], + "transactionHash": HexBytes( + "0x5f790334bab22d8f621d8da02f3d9af103f80d4bbc6843ef2f2680a4f9531b71" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xde160b1ae8e89ab7fc20b61cc5d8ee8944fdd29826470f301b89cbaf14a6aa1f" + ), + "blockNumber": 86, + "data": "0x000000000000000000000000cb03e30c7bd004a5eca1d88b55409033b825eb91", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0x1151116914515bc0891ff9047a6cb32cf902546f83066499bcf8ba33d2353fa2" + ) + ], + "transactionHash": HexBytes( + "0x5f790334bab22d8f621d8da02f3d9af103f80d4bbc6843ef2f2680a4f9531b71" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x0059c65c3d2325D77E9288E022D24d3972b1799D", + "blockHash": HexBytes( + "0xde160b1ae8e89ab7fc20b61cc5d8ee8944fdd29826470f301b89cbaf14a6aa1f" + ), + "blockNumber": 86, + "data": "0xf1130e1edda13f929107c7e99157a777af1d8f485c8745faa1b933779a0669920000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 2, + "removed": False, + "topics": [ + HexBytes( + "0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e" + ) + ], + "transactionHash": HexBytes( + "0x5f790334bab22d8f621d8da02f3d9af103f80d4bbc6843ef2f2680a4f9531b71" + ), + "transactionIndex": 0, + } + ), +] diff --git a/safe_transaction_service/history/tests/test_erc20_events_indexer.py b/safe_transaction_service/history/tests/test_erc20_events_indexer.py index f27a7a501..8b7b16c9a 100644 --- a/safe_transaction_service/history/tests/test_erc20_events_indexer.py +++ b/safe_transaction_service/history/tests/test_erc20_events_indexer.py @@ -1,18 +1,23 @@ -import copy -from unittest import mock - from django.test import TestCase from gnosis.eth.tests.ethereum_test_case import EthereumTestCaseMixin -from ..indexers import Erc20EventsIndexer, Erc20EventsIndexerProvider +from ..indexers import Erc20EventsIndexerProvider from ..models import ERC20Transfer, EthereumTx, IndexingStatus -from .factories import SafeContractFactory +from .factories import EthereumTxFactory, SafeContractFactory +from .mocks.mocks_erc20_events_indexer import log_receipt_mock class TestErc20EventsIndexer(EthereumTestCaseMixin, TestCase): + def setUp(self) -> None: + Erc20EventsIndexerProvider.del_singleton() + self.erc20_events_indexer = Erc20EventsIndexerProvider() + + def tearDown(self) -> None: + Erc20EventsIndexerProvider.del_singleton() + def test_erc20_events_indexer(self): - erc20_events_indexer = Erc20EventsIndexerProvider() + erc20_events_indexer = self.erc20_events_indexer erc20_events_indexer.confirmations = 0 self.assertEqual(erc20_events_indexer.start(), (0, 0)) @@ -53,47 +58,36 @@ def test_erc20_events_indexer(self): ERC20Transfer.objects.to_or_from(safe_contract.address).count(), 1 ) - # Test _process_decoded_element block_number = self.ethereum_client.get_transaction(tx_hash)["blockNumber"] event = self.ethereum_client.erc20.get_total_transfer_history( from_block=block_number, to_block=block_number )[0] self.assertIn("value", event["args"]) - original_event = copy.deepcopy(event) - event["args"]["unknown"] = event["args"].pop("value") + def test_mark_as_processed(self): + # Create transaction in db so not fetching of transaction is needed + for log_receipt in log_receipt_mock: + tx_hash = log_receipt["transactionHash"] + block_hash = log_receipt["blockHash"] + EthereumTxFactory(tx_hash=tx_hash, block__block_hash=block_hash) + # After the first processing transactions will be cached to prevent reprocessing + self.assertEqual(len(self.erc20_events_indexer._processed_element_cache), 0) self.assertEqual( - erc20_events_indexer._process_decoded_element(event), original_event + len(self.erc20_events_indexer.process_elements(log_receipt_mock)), 1 ) + self.assertEqual(len(self.erc20_events_indexer._processed_element_cache), 1) - # Test ERC721 - event = self.ethereum_client.erc20.get_total_transfer_history( - from_block=block_number, to_block=block_number - )[0] - with mock.patch.object( - Erc20EventsIndexer, "_is_erc20", autospec=True, return_value=False - ): - # Convert event to erc721 - event["args"]["tokenId"] = event["args"].pop("value") - original_event = copy.deepcopy(event) - event["args"]["unknown"] = event["args"].pop("tokenId") - - self.assertEqual( - erc20_events_indexer._process_decoded_element(event), original_event - ) + # Transactions are cached and will not be reprocessed + self.assertEqual( + len(self.erc20_events_indexer.process_elements(log_receipt_mock)), 0 + ) + self.assertEqual( + len(self.erc20_events_indexer.process_elements(log_receipt_mock)), 0 + ) - event = self.ethereum_client.erc20.get_total_transfer_history( - from_block=block_number, to_block=block_number - )[0] - with mock.patch.object( - Erc20EventsIndexer, "_is_erc20", autospec=True, return_value=True - ): - # Convert event to erc721 - original_event = copy.deepcopy(event) - event["args"]["tokenId"] = event["args"].pop("value") - - # ERC721 event will be converted to ERC20 - self.assertEqual( - erc20_events_indexer._process_decoded_element(event), original_event - ) + # Cleaning the cache will reprocess the transactions again + self.erc20_events_indexer._processed_element_cache.clear() + self.assertEqual( + len(self.erc20_events_indexer.process_elements(log_receipt_mock)), 1 + ) diff --git a/safe_transaction_service/history/tests/test_internal_tx_indexer.py b/safe_transaction_service/history/tests/test_internal_tx_indexer.py index 7063c9403..b16256a3b 100644 --- a/safe_transaction_service/history/tests/test_internal_tx_indexer.py +++ b/safe_transaction_service/history/tests/test_internal_tx_indexer.py @@ -24,7 +24,7 @@ SafeMasterCopy, SafeStatus, ) -from .factories import SafeMasterCopyFactory +from .factories import EthereumTxFactory, SafeMasterCopyFactory from .mocks.mocks_internal_tx_indexer import ( block_result, trace_blocks_filtered_0x5aC2_result, @@ -37,15 +37,12 @@ class TestInternalTxIndexer(TestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.internal_tx_indexer = InternalTxIndexerProvider() - cls.internal_tx_indexer.blocks_to_reindex_again = 0 - - @classmethod - def tearDownClass(cls): - super().tearDownClass() + def setUp(self) -> None: + InternalTxIndexerProvider.del_singleton() + self.internal_tx_indexer = InternalTxIndexerProvider() + self.internal_tx_indexer.blocks_to_reindex_again = 0 + + def tearDown(self) -> None: InternalTxIndexerProvider.del_singleton() def test_internal_tx_indexer_provider(self): @@ -61,6 +58,12 @@ def test_internal_tx_indexer_provider(self): ) def return_sorted_blocks(self, hashes: HexStr): + """ + Mock function helper + + :param hashes: + :return: + """ block_dict = {block["hash"].hex(): block for block in block_result} return [block_dict[provided_hash] for provided_hash in hashes] @@ -316,3 +319,37 @@ def test_tx_processor_using_internal_tx_indexer_with_existing_safe(self): self.assertEqual(internal_txs_decoded[0].function_name, "setup") results = tx_processor.process_decoded_transactions(internal_txs_decoded) self.assertEqual(results, [True, True]) + + def test_mark_as_processed(self): + """ + Test not reprocessing of processed events + """ + + # Transform mock to dictionary tx_hash -> traces + tx_hash_with_traces = {} + for trace_transaction_result in trace_transactions_result: + tx_hash = trace_transaction_result[0]["transactionHash"] + tx_hash_with_traces[tx_hash] = trace_transaction_result + # Create transaction in db so not fetching of transaction is needed + EthereumTxFactory(tx_hash=tx_hash) + + # After the first processing transactions will be cached to prevent reprocessing + self.assertEqual(len(self.internal_tx_indexer._processed_element_cache), 0) + self.assertEqual( + len(self.internal_tx_indexer.process_elements(tx_hash_with_traces)), 2 + ) + self.assertEqual(len(self.internal_tx_indexer._processed_element_cache), 2) + + # Transactions are cached and will not be reprocessed + self.assertEqual( + len(self.internal_tx_indexer.process_elements(tx_hash_with_traces)), 0 + ) + self.assertEqual( + len(self.internal_tx_indexer.process_elements(tx_hash_with_traces)), 0 + ) + + # Cleaning the cache will reprocess the transactions again + self.internal_tx_indexer._processed_element_cache.clear() + self.assertEqual( + len(self.internal_tx_indexer.process_elements(tx_hash_with_traces)), 2 + ) diff --git a/safe_transaction_service/history/tests/test_safe_events_indexer.py b/safe_transaction_service/history/tests/test_safe_events_indexer.py index d3edff3a9..a7951aad5 100644 --- a/safe_transaction_service/history/tests/test_safe_events_indexer.py +++ b/safe_transaction_service/history/tests/test_safe_events_indexer.py @@ -13,6 +13,7 @@ from ..indexers import SafeEventsIndexer, SafeEventsIndexerProvider from ..indexers.tx_processor import SafeTxProcessor from ..models import ( + EthereumTx, EthereumTxCallType, InternalTx, InternalTxDecoded, @@ -22,17 +23,19 @@ SafeLastStatus, SafeStatus, ) -from .factories import SafeMasterCopyFactory +from .factories import EthereumTxFactory, SafeMasterCopyFactory +from .mocks.mocks_safe_events_indexer import safe_events_mock class TestSafeEventsIndexer(SafeTestCaseMixin, TestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.safe_events_indexer = SafeEventsIndexer( - cls.ethereum_client, confirmations=0, blocks_to_reindex_again=0 + def setUp(self) -> None: + self.safe_events_indexer = SafeEventsIndexer( + self.ethereum_client, confirmations=0, blocks_to_reindex_again=0 ) - cls.safe_tx_processor = SafeTxProcessor(cls.ethereum_client, None) + self.safe_tx_processor = SafeTxProcessor(self.ethereum_client, None) + + def tearDown(self) -> None: + SafeEventsIndexerProvider.del_singleton() def test_safe_events_indexer_provider(self): safe_events_indexer = SafeEventsIndexerProvider() @@ -606,3 +609,35 @@ def test_safe_events_indexer(self): self.assertEqual( InternalTxDecoded.objects.count(), expected_internal_txs_decoded ) + + def test_mark_as_processed(self): + # SafeEventsIndexer does not use bulk saving into database, + # so mark_as_processed is just a optimization but not critical + + # Create transaction in db so not fetching of transaction is needed + for safe_event in safe_events_mock: + tx_hash = safe_event["transactionHash"] + block_hash = safe_event["blockHash"] + if not EthereumTx.objects.filter(tx_hash=tx_hash).exists(): + EthereumTxFactory(tx_hash=tx_hash, block__block_hash=block_hash) + + # After the first processing transactions will be cached to prevent reprocessing + self.assertEqual(len(self.safe_events_indexer._processed_element_cache), 0) + self.assertEqual( + len(self.safe_events_indexer.process_elements(safe_events_mock)), 28 + ) + self.assertEqual(len(self.safe_events_indexer._processed_element_cache), 28) + + # Transactions are cached and will not be reprocessed + self.assertEqual( + len(self.safe_events_indexer.process_elements(safe_events_mock)), 0 + ) + self.assertEqual( + len(self.safe_events_indexer.process_elements(safe_events_mock)), 0 + ) + + # Even if we empty the cache, events will not be reprocessed again + self.safe_events_indexer._processed_element_cache.clear() + self.assertEqual( + len(self.safe_events_indexer.process_elements(safe_events_mock)), 0 + ) diff --git a/safe_transaction_service/utils/utils.py b/safe_transaction_service/utils/utils.py index 29dcc9330..04571f459 100644 --- a/safe_transaction_service/utils/utils.py +++ b/safe_transaction_service/utils/utils.py @@ -8,6 +8,24 @@ from gevent.monkey import saved +class FixedSizeDict(dict): + """ + Fixed size dictionary to be used as an LRU cache + + Dictionaries are guaranteed to be insertion sorted from Python 3.7 onwards + """ + + def __init__(self, *args, maxlen=0, **kwargs): + self._maxlen = maxlen + super().__init__(*args, **kwargs) + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + if self._maxlen > 0: + if len(self) > self._maxlen: + self.pop(next(iter(self))) + + def chunks(elements: List[Any], n: int) -> Iterable[Any]: """ :param elements: List