From 8f96b0b157f6e90b3821ae95f2bec2b1ea2d8fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s?= <7888669+moisses89@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:31:15 +0200 Subject: [PATCH] Optimize safe events indexer (#2194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Uxío --- .../history/indexers/events_indexer.py | 12 +- .../history/indexers/safe_events_indexer.py | 313 ++++++++++++++---- .../tests/mocks/mocks_safe_events_indexer.py | 96 ++++++ .../history/tests/test_safe_events_indexer.py | 228 ++++++++++++- 4 files changed, 575 insertions(+), 74 deletions(-) diff --git a/safe_transaction_service/history/indexers/events_indexer.py b/safe_transaction_service/history/indexers/events_indexer.py index d31a359ed..6f9701f9f 100644 --- a/safe_transaction_service/history/indexers/events_indexer.py +++ b/safe_transaction_service/history/indexers/events_indexer.py @@ -248,6 +248,13 @@ def decode_elements(self, log_receipts: Sequence[LogReceipt]) -> List[EventData] decoded_elements.append(decoded_element) return decoded_elements + def _process_decoded_elements(self, decoded_elements: List[EventData]) -> List[Any]: + processed_elements = [] + for decoded_element in decoded_elements: + if processed_element := self._process_decoded_element(decoded_element): + processed_elements.append(processed_element) + return processed_elements + def process_elements(self, log_receipts: Sequence[LogReceipt]) -> List[Any]: """ Process all events found by `find_relevant_elements` @@ -281,10 +288,7 @@ def process_elements(self, log_receipts: Sequence[LogReceipt]) -> List[Any]: self.index_service.txs_create_or_update_from_tx_hashes(tx_hashes) logger.debug("End prefetching and storing of ethereum txs") logger.debug("Processing %d decoded events", len(decoded_elements)) - processed_elements = [] - for decoded_element in decoded_elements: - if processed_element := self._process_decoded_element(decoded_element): - processed_elements.append(processed_element) + processed_elements = self._process_decoded_elements(decoded_elements) logger.debug("End processing %d decoded events", len(decoded_elements)) logger.debug("Marking events as processed") diff --git a/safe_transaction_service/history/indexers/safe_events_indexer.py b/safe_transaction_service/history/indexers/safe_events_indexer.py index 00362cc1d..b23299541 100644 --- a/safe_transaction_service/history/indexers/safe_events_indexer.py +++ b/safe_transaction_service/history/indexers/safe_events_indexer.py @@ -1,6 +1,6 @@ from functools import cached_property from logging import getLogger -from typing import List, Optional +from typing import Any, Dict, List, Optional from django.db import IntegrityError, transaction @@ -287,6 +287,54 @@ def _is_setup_indexed(self, safe_address: ChecksumAddress): def decode_elements(self, *args) -> List[EventData]: return super().decode_elements(*args) + def _get_internal_tx_from_decoded_event( + self, decoded_event: EventData, **kwargs + ) -> InternalTx: + """ + Creates an InternalTx instance from the given decoded_event. + Allows overriding object parameters with additional keyword arguments. + + :param decoded_event: + :param kwargs: + :return: + """ + ethereum_tx_hash = HexBytes(decoded_event["transactionHash"]) + ethereum_block_number = decoded_event["blockNumber"] + ethereum_block_timestamp = EthereumBlock.objects.get_timestamp_by_hash( + decoded_event["blockHash"] + ) + address = decoded_event["address"] + default_trace_address = str(decoded_event["logIndex"]) + + # Setting default values + internal_tx = InternalTx( + ethereum_tx_id=ethereum_tx_hash, + timestamp=ethereum_block_timestamp, + block_number=ethereum_block_number, + _from=address, + gas=50000, + data=b"", + to=decoded_event["args"].get("to", NULL_ADDRESS), + value=decoded_event["args"].get("value", 0), + gas_used=50000, + contract_address=None, + code=None, + output=None, + refund_address=None, + tx_type=InternalTxType.CALL.value, + call_type=EthereumTxCallType.CALL.value, + trace_address=default_trace_address, + error=None, + ) + # Overriding passed keys + for key, value in kwargs.items(): + if hasattr(internal_tx, key): + setattr(internal_tx, key, value) + else: + raise AttributeError(f"Invalid atribute {key} for InternalTx") + + return internal_tx + @transaction.atomic def _process_decoded_element( self, decoded_element: EventData @@ -299,7 +347,6 @@ def _process_decoded_element( args = dict(decoded_element["args"]) ethereum_tx_hash = HexBytes(decoded_element["transactionHash"]) ethereum_tx_hash_hex = ethereum_tx_hash.hex() - ethereum_block_number = decoded_element["blockNumber"] ethereum_block_timestamp = EthereumBlock.objects.get_timestamp_by_hash( decoded_element["blockHash"] ) @@ -311,24 +358,11 @@ def _process_decoded_element( decoded_element, ) - internal_tx = InternalTx( - ethereum_tx_id=ethereum_tx_hash, - timestamp=ethereum_block_timestamp, - block_number=ethereum_block_number, - _from=safe_address, - gas=50000, - data=b"", - to=NULL_ADDRESS, # It should be Master copy address but we cannot detect it - value=0, - gas_used=50000, - contract_address=None, - code=None, - output=None, - refund_address=None, - tx_type=InternalTxType.CALL.value, + internal_tx = self._get_internal_tx_from_decoded_event( + decoded_element, call_type=EthereumTxCallType.DELEGATE_CALL.value, - trace_address=trace_address, - error=None, + to=NULL_ADDRESS, + value=0, ) child_internal_tx: Optional[InternalTx] = None # For Ether transfers internal_tx_decoded = InternalTxDecoded( @@ -336,40 +370,11 @@ def _process_decoded_element( function_name="", arguments=args, ) - if event_name == "ProxyCreation": - # Should be the 2nd event to be indexed, after `SafeSetup` - safe_address = args.pop("proxy") - if self._is_setup_indexed(safe_address): - internal_tx = None - else: - new_trace_address = f"{trace_address},0" - to = args.pop("singleton") - - # Try to update InternalTx created by SafeSetup (if Safe was created using the ProxyFactory) with - # the master copy used. Without tracing it cannot be detected otherwise - InternalTx.objects.filter( - contract_address=safe_address, decoded_tx__function_name="setup" - ).update(to=to, contract_address=None, trace_address=new_trace_address) - # Add creation internal tx. _from is the address of the proxy instead of the safe_address - internal_tx.contract_address = safe_address - internal_tx.tx_type = InternalTxType.CREATE.value - internal_tx.call_type = None - internal_tx_decoded = None - elif event_name == "SafeSetup": - # Should be the 1st event to be indexed, unless custom `to` and `data` are set - if self._is_setup_indexed(safe_address): - internal_tx = None - else: - # Usually ProxyCreation is called before SafeSetup, but it can be the opposite if someone - # creates a Safe and configure it in the next transaction. Remove it if that's the case - InternalTx.objects.filter(contract_address=safe_address).delete() - internal_tx.contract_address = safe_address - internal_tx_decoded.function_name = "setup" - args["payment"] = 0 - args["paymentReceiver"] = NULL_ADDRESS - args["_threshold"] = args.pop("threshold") - args["_owners"] = args.pop("owners") + if event_name == "ProxyCreation" or event_name == "SafeSetup": + # Will ignore these events because were indexed in process_safe_creation_events + internal_tx = None + elif event_name == "SafeMultiSigTransaction": internal_tx_decoded.function_name = "execTransaction" data = HexBytes(args["data"]) @@ -392,25 +397,13 @@ def _process_decoded_element( additional_info.hex(), ) if args["value"] and not data: # Simulate ether transfer - child_internal_tx = InternalTx( - ethereum_tx_id=ethereum_tx_hash, - timestamp=ethereum_block_timestamp, - block_number=ethereum_block_number, - _from=safe_address, + child_internal_tx = self._get_internal_tx_from_decoded_event( + decoded_element, gas=23000, - data=b"", - to=args["to"], - value=args["value"], gas_used=23000, - contract_address=None, - code=None, - output=None, - refund_address=None, - tx_type=InternalTxType.CALL.value, - call_type=EthereumTxCallType.CALL.value, trace_address=f"{trace_address},0", - error=None, ) + elif event_name == "SafeModuleTransaction": internal_tx_decoded.function_name = "execTransactionFromModule" args["data"] = HexBytes(args["data"]).hex() @@ -485,3 +478,189 @@ def _process_decoded_element( ) return internal_tx + + def _get_safe_creation_events( + self, decoded_elements: List[EventData] + ) -> Dict[ChecksumAddress, List[EventData]]: + """ + Get the creation events (ProxyCreation and SafeSetup) from decoded elements and generates a dictionary that groups these events by Safe address. + + :param decoded_elements: + :return: + """ + safe_creation_events: Dict[ChecksumAddress, List[Dict]] = {} + for decoded_element in decoded_elements: + event_name = decoded_element["event"] + if event_name == "SafeSetup": + safe_address = decoded_element["address"] + safe_creation_events.setdefault(safe_address, []).append( + decoded_element + ) + elif event_name == "ProxyCreation": + safe_address = decoded_element["args"].get("proxy") + safe_creation_events.setdefault(safe_address, []).append( + decoded_element + ) + + return safe_creation_events + + def _process_safe_creation_events( + self, + safe_addresses_with_creation_events: Dict[ChecksumAddress, List[EventData]], + ) -> List[InternalTx]: + """ + Process creation events (ProxyCreation and SafeSetup). + + :param safe_addresses_with_creation_events: + :return: + """ + internal_txs = [] + internal_decoded_txs = [] + # Check if were indexed + safe_creation_events_addresses = set(safe_addresses_with_creation_events.keys()) + indexed_addresses = InternalTxDecoded.objects.filter( + internal_tx___from__in=safe_creation_events_addresses, + function_name="setup", + internal_tx__contract_address=None, + ).values_list("internal_tx___from", flat=True) + # Ignoring the already indexed contracts + addresses_to_index = safe_creation_events_addresses - set(indexed_addresses) + + for safe_address in addresses_to_index: + events = safe_addresses_with_creation_events[safe_address] + for event_position, event in enumerate(events): + if event["event"] == "SafeSetup": + setup_event = event + # If we have both events we should extract Singleton and trace_address from ProxyCreation event + if len(events) > 1: + if ( + event_position == 0 + and events[1]["event"] == "ProxyCreation" + ): + # Usually SafeSetup is the first event and next is ProxyCreation when ProxyFactory is called with initializer. + proxy_creation_event = events[1] + elif ( + event_position == 1 + and events[0]["event"] == "ProxyCreation" + ): + # ProxyCreation first and SafeSetup later + proxy_creation_event = events[0] + else: + # Proxy was created in previous blocks. + proxy_creation_event = None + # Safe was created and configure it in the next transaction. Remove it if that's the case + InternalTx.objects.filter( + contract_address=safe_address + ).delete() + + # Generate InternalTx and internalDecodedTx for SafeSetup event + setup_trace_address = ( + f"{str(proxy_creation_event['logIndex'])},0" + if proxy_creation_event + else str(setup_event["logIndex"]) + ) + singleton = ( + proxy_creation_event["args"].get("singleton") + if proxy_creation_event + else NULL_ADDRESS + ) + # Keep previous implementation + contract_address = None if proxy_creation_event else safe_address + internal_tx = self._get_internal_tx_from_decoded_event( + setup_event, + contract_address=contract_address, + to=singleton, + trace_address=setup_trace_address, + call_type=EthereumTxCallType.DELEGATE_CALL.value, + ) + # Generate InternalDecodedTx for SafeSetup event + setup_args = dict(event["args"]) + setup_args["payment"] = 0 + setup_args["paymentReceiver"] = NULL_ADDRESS + setup_args["_threshold"] = setup_args.pop("threshold") + setup_args["_owners"] = setup_args.pop("owners") + internal_tx_decoded = InternalTxDecoded( + internal_tx=internal_tx, + function_name="setup", + arguments=setup_args, + ) + internal_txs.append(internal_tx) + internal_decoded_txs.append(internal_tx_decoded) + elif event["event"] == "ProxyCreation": + proxy_creation_event = event + # Generate InternalTx for ProxyCreation + internal_tx = self._get_internal_tx_from_decoded_event( + proxy_creation_event, + contract_address=proxy_creation_event["args"].get("proxy"), + tx_type=InternalTxType.CREATE.value, + call_type=None, + ) + internal_txs.append(internal_tx) + else: + logger.error(f"Event is not a Safe creation event {event['event']}") + + # Store which internal txs where inserted into database + stored_internal_txs: List[InternalTx] = [] + with transaction.atomic(): + try: + InternalTx.objects.bulk_create(internal_txs) + InternalTxDecoded.objects.bulk_create(internal_decoded_txs) + stored_internal_txs = internal_txs + except IntegrityError: + logger.info( + "Cannot bulk create the provided Safe Creation Events, trying one by one" + ) + + if internal_txs and not stored_internal_txs: + # Fallback handler in case of integrity error + for internal_decoded_tx in internal_decoded_txs: + try: + with transaction.atomic(): + # Insert first the internal_tx with internalDecodedTx relation + InternalTx.save(internal_decoded_tx.internal_tx) + InternalTxDecoded.save(internal_decoded_tx) + # Remove inserted internal transactions from the list + internal_txs.remove(internal_decoded_tx.internal_tx) + stored_internal_txs.append(internal_tx) + except IntegrityError: + logger.info( + "Ignoring already processed InternalTx with tx-hash=%s and trace-address=%s", + internal_decoded_tx.internal_tx.ethereum_tx_id.hex(), + internal_decoded_tx.internal_tx.trace_address, + ) + pass + # Insert the remaining InternalTxs + for internal_tx in internal_txs: + try: + with transaction.atomic(): + InternalTx.save(internal_tx) + stored_internal_txs.append(internal_tx) + except IntegrityError: + logger.info( + "Ignoring already processed InternalTx with tx-hash=%s and trace-address=%s", + internal_tx.ethereum_tx_id.hex(), + internal_tx.trace_address, + ) + pass + + return stored_internal_txs + + def _process_decoded_elements(self, decoded_elements: list[EventData]) -> list[Any]: + processed_elements = [] + # Extract Safe creation events by Safe from decoded_elements list + safe_addresses_creation_events = self._get_safe_creation_events( + decoded_elements + ) + if safe_addresses_creation_events: + # Process safe creation events + creation_events_processed = self._process_safe_creation_events( + safe_addresses_creation_events + ) + processed_elements.extend(creation_events_processed) + + # Process the rest of Safe events + for decoded_element in decoded_elements: + if processed_element := self._process_decoded_element(decoded_element): + processed_elements.append(processed_element) + + return processed_elements 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 index 3861f6915..aebd748ac 100644 --- a/safe_transaction_service/history/tests/mocks/mocks_safe_events_indexer.py +++ b/safe_transaction_service/history/tests/mocks/mocks_safe_events_indexer.py @@ -603,3 +603,99 @@ } ), ] + +setup_events_mock = [ + AttributeDict( + { + "address": "0x33310eeBb69B19963dA4a16Aeafac78AB6901fbB", + "blockHash": HexBytes( + "0xdfa4d85183c64488211b6a270a9b1231463d014538fe61586fe83a650ca62576" + ), + "blockNumber": 76, + "data": "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x141df868a6331af528e38c83b7aa03edc19be66e37ae67f9285bf4f8e3c6a1a8" + ), + HexBytes( + "0x000000000000000000000000a21e2615ed32ce9ddfc53a1b0ccfe689e9152f25" + ), + ], + "transactionHash": HexBytes( + "0x2b4d9886e32fdd5b05072b592c7b5d2e50020282cd7d234a2054e71458887c36" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + "blockHash": HexBytes( + "0x1ab0d0bcde388ace7010770e9c6fa19c8840ea8f6134c93e52d91b08b44a327f" + ), + "blockNumber": 76, + "data": "0x0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b", + "logIndex": 0, + "removed": False, + "topics": [ + HexBytes( + "0x141df868a6331af528e38c83b7aa03edc19be66e37ae67f9285bf4f8e3c6a1a8" + ), + HexBytes( + "0x000000000000000000000000a21e2615ed32ce9ddfc53a1b0ccfe689e9152f25" + ), + ], + "transactionHash": HexBytes( + "0xd2ccece7094e36e82acc441a163d2f07dbfb82f1948988d89fef9d941fb2a890" + ), + "transactionIndex": 0, + } + ), +] + +proxy_creation_event_mock = [ + AttributeDict( + { + "address": "0xA21E2615ED32CE9DdFc53A1B0ccFE689e9152f25", + "blockHash": HexBytes( + "0xdfa4d85183c64488211b6a270a9b1231463d014538fe61586fe83a650ca62576" + ), + "blockNumber": 76, + "data": "0x000000000000000000000000999e362288fa8313c56b59e7ab0ead4afa92441e0000000000000000000000009c82244a30cd4b2591f9a8e8ad0dab0fa643ee34", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0x4f51faf6c4561ff95f067657e43439f0f856d97c04d9ec9070a6199ad418e235" + ) + ], + "transactionHash": HexBytes( + "0x2b4d9886e32fdd5b05072b592c7b5d2e50020282cd7d234a2054e71458887c36" + ), + "transactionIndex": 0, + } + ), + AttributeDict( + { + "address": "0xA21E2615ED32CE9DdFc53A1B0ccFE689e9152f25", + "blockHash": HexBytes( + "0x34920c133603319a4b40255b3f20512172aa58388b27ac8bb06dfed3aad8135f" + ), + "blockNumber": 77, + "data": "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad0000000000000000000000009c82244a30cd4b2591f9a8e8ad0dab0fa643ee34", + "logIndex": 1, + "removed": False, + "topics": [ + HexBytes( + "0x4f51faf6c4561ff95f067657e43439f0f856d97c04d9ec9070a6199ad418e235" + ) + ], + "transactionHash": HexBytes( + "0x0e7cba26c17906a9e752198a0b09a5a3e25866055a4684334fd70affc866ec07" + ), + "transactionIndex": 0, + } + ), +] 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 8ab6f3db8..f194b6f43 100644 --- a/safe_transaction_service/history/tests/test_safe_events_indexer.py +++ b/safe_transaction_service/history/tests/test_safe_events_indexer.py @@ -4,6 +4,7 @@ from eth_typing import ChecksumAddress from hexbytes import HexBytes from web3 import Web3 +from web3.auto import w3 from web3.datastructures import AttributeDict from web3.types import LogReceipt @@ -25,8 +26,12 @@ SafeLastStatus, SafeStatus, ) -from .factories import EthereumTxFactory, SafeMasterCopyFactory -from .mocks.mocks_safe_events_indexer import safe_events_mock +from .factories import EthereumBlockFactory, EthereumTxFactory, SafeMasterCopyFactory +from .mocks.mocks_safe_events_indexer import ( + proxy_creation_event_mock, + safe_events_mock, + setup_events_mock, +) class TestSafeEventsIndexerV1_4_1(SafeTestCaseMixin, TestCase): @@ -217,7 +222,7 @@ def test_safe_events_indexer(self): self.assertEqual(InternalTx.objects.count(), 0) self.assertEqual(InternalTxDecoded.objects.count(), 0) self.assertEqual(self.safe_events_indexer.start(), (2, 1)) - self.assertEqual(InternalTxDecoded.objects.count(), 1) + self.assertEqual(InternalTxDecoded.objects.count(), 1) # Just setup is decoded self.assertEqual(InternalTx.objects.count(), 2) # Proxy factory and setup create_internal_tx = InternalTx.objects.filter( contract_address=safe_address @@ -707,6 +712,223 @@ def test_auto_adjust_block_limit(self): pass self.assertEqual(self.safe_events_indexer.block_process_limit, 5) + def test_get_safe_creation_events(self): + decoded_elements = self.safe_events_indexer.decode_elements(safe_events_mock) + self.assertEqual(len(decoded_elements), 28) + safe_creation_events = self.safe_events_indexer._get_safe_creation_events( + decoded_elements + ) + self.assertEqual(len(safe_creation_events), 1) + safe_address = "0x0059c65c3d2325D77E9288E022D24d3972b1799D" # Safe address created in safe_events_mock + self.assertEqual(len(safe_creation_events[safe_address]), 2) + self.assertEqual(safe_creation_events[safe_address][0]["event"], "SafeSetup") + self.assertEqual( + safe_creation_events[safe_address][1]["event"], "ProxyCreation" + ) + + # Add a ProxyCreation and SafeSetup event for different safe address + modified_safe_events_mock = safe_events_mock.copy() # Avoid race conditions + modified_safe_events_mock.append(proxy_creation_event_mock[0]) + modified_safe_events_mock.append(setup_events_mock[0]) + decoded_elements = self.safe_events_indexer.decode_elements( + modified_safe_events_mock + ) + safe_creation_events = self.safe_events_indexer._get_safe_creation_events( + decoded_elements + ) + self.assertEqual(len(safe_creation_events), 3) + new_setup_event = "0x33310eeBb69B19963dA4a16Aeafac78AB6901fbB" + self.assertEqual(len(safe_creation_events[new_setup_event]), 1) + self.assertEqual(safe_creation_events[new_setup_event][0]["event"], "SafeSetup") + new_proxy_event = "0x999E362288fA8313c56b59e7AB0ead4afA92441e" + self.assertEqual(len(safe_creation_events[new_proxy_event]), 1) + self.assertEqual( + safe_creation_events[new_proxy_event][0]["event"], "ProxyCreation" + ) + # Previous events should remains equal + self.assertEqual(len(safe_creation_events[safe_address]), 2) + self.assertEqual(safe_creation_events[safe_address][0]["event"], "SafeSetup") + self.assertEqual( + safe_creation_events[safe_address][1]["event"], "ProxyCreation" + ) + + def test_proxy_creation_event_without_initializer(self): + + initial_block_number = self.ethereum_client.current_block_number + 1 + safe_l2_master_copy = SafeMasterCopyFactory( + address=self.safe_contract.address, + initial_block_number=initial_block_number, + tx_block_number=initial_block_number, + version=self.safe_contract_version, + l2=True, + ) + ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract_with_nonce( + self.ethereum_test_account, + self.safe_contract.address, + initializer=b"", + ) + safe_address = ethereum_tx_sent.contract_address + self.assertEqual(InternalTx.objects.count(), 0) + self.assertEqual(InternalTxDecoded.objects.count(), 0) + self.assertEqual(self.safe_events_indexer.start(), (1, 1)) + self.assertEqual( + InternalTxDecoded.objects.count(), 0 + ) # Just created without setup + self.assertEqual(InternalTx.objects.count(), 1) # Proxy factory + # Proxy creation InternalTx must contain the Safe address + self.assertEqual( + InternalTx.objects.filter(contract_address=safe_address).count(), 1 + ) + # Call setup + owner_account_1 = self.ethereum_test_account + owners = [owner_account_1.address] + threshold = 1 + to = NULL_ADDRESS + data = b"" + fallback_handler = NULL_ADDRESS + payment_token = NULL_ADDRESS + payment = 0 + payment_receiver = NULL_ADDRESS + deployed_safe_contract = get_safe_V1_4_1_contract(self.w3, safe_address) + setup_call = deployed_safe_contract.functions.setup( + owners, + threshold, + to, + data, + fallback_handler, + payment_token, + payment, + payment_receiver, + ).build_transaction( + {"nonce": self.w3.eth.get_transaction_count(owner_account_1.address)} + ) + signed_tx = owner_account_1.sign_transaction(setup_call) + tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) + self.assertEqual(self.safe_events_indexer.start(), (1, 1)) + # We remove the proxyCreation internaltx + self.assertEqual(InternalTx.objects.count(), 1) + self.assertEqual(InternalTxDecoded.objects.count(), 1) + # TODO get singleton address when setup and creation are not indexed together + # self.assertEqual(InternalTx.objects.filter(contract_address=None, to=self.safe_contract.address).count(), 1) + # ProxyCreation first and SafeSetup later indexed together + ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract_with_nonce( + self.ethereum_test_account, + self.safe_contract_V1_3_0.address, + initializer=b"", + ) + safe_address = ethereum_tx_sent.contract_address + deployed_safe_contract = get_safe_V1_4_1_contract(self.w3, safe_address) + setup_call = deployed_safe_contract.functions.setup( + owners, + threshold, + to, + data, + fallback_handler, + payment_token, + payment, + payment_receiver, + ).build_transaction( + {"nonce": self.w3.eth.get_transaction_count(owner_account_1.address)} + ) + signed_tx = owner_account_1.sign_transaction(setup_call) + tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) + self.assertEqual(self.safe_events_indexer.start(), (2, 2)) + # Proxy creation InternalTx must contain the Safe address + self.assertEqual( + InternalTx.objects.filter(contract_address=safe_address).count(), 1 + ) + self.assertEqual( + InternalTx.objects.filter( + contract_address=None, to=self.safe_contract_V1_3_0.address + ).count(), + 1, + ) + + def test_process_safe_creation_events_forcing_duplicate_events(self): + # Insert a lonely ProxyCreation event + proxy_creation_events = self.safe_events_indexer.decode_elements( + proxy_creation_event_mock[:1] + ) + block = EthereumBlockFactory(block_hash=proxy_creation_events[0]["blockHash"]) + EthereumTxFactory( + tx_hash=proxy_creation_events[0]["transactionHash"], block=block + ) + self.assertEqual(len(proxy_creation_events), 1) + safe_creation_events = self.safe_events_indexer._get_safe_creation_events( + proxy_creation_events + ) + self.assertEqual(len(safe_creation_events), 1) + internal_txs = self.safe_events_indexer._process_safe_creation_events( + safe_creation_events, + ) + self.assertEqual(len(internal_txs), 1) + self.assertEqual(InternalTx.objects.count(), 1) + self.assertEqual(InternalTxDecoded.objects.count(), 0) + + # Insert the same previous proxy creation event shouldn't be possible + internal_txs = self.safe_events_indexer._process_safe_creation_events( + safe_creation_events, + ) + self.assertEqual(len(internal_txs), 0) + self.assertEqual(InternalTx.objects.count(), 1) + self.assertEqual(InternalTxDecoded.objects.count(), 0) + + # Add new proxyCreation to the events list + proxy_creation_events = self.safe_events_indexer.decode_elements( + proxy_creation_event_mock[:2] + ) + proxy_block = EthereumBlockFactory( + block_hash=proxy_creation_events[1]["blockHash"] + ) + EthereumTxFactory( + tx_hash=proxy_creation_events[1]["transactionHash"], block=proxy_block + ) + self.assertEqual(len(proxy_creation_events), 2) + safe_creation_events = self.safe_events_indexer._get_safe_creation_events( + proxy_creation_events + ) + self.assertEqual(len(safe_creation_events), 2) + internal_txs = self.safe_events_indexer._process_safe_creation_events( + safe_creation_events, + ) + self.assertEqual(len(internal_txs), 1) + self.assertEqual(InternalTx.objects.count(), 2) + self.assertEqual(InternalTxDecoded.objects.count(), 0) + + # Try to insert the same events shouldn't insert anything + internal_txs = self.safe_events_indexer._process_safe_creation_events( + safe_creation_events, + ) + self.assertEqual(len(internal_txs), 0) + self.assertEqual(InternalTx.objects.count(), 2) + self.assertEqual(InternalTxDecoded.objects.count(), 0) + + # Add setup event to the last proxyCreation event + creation_events_mock = proxy_creation_event_mock + creation_events_mock.append(setup_events_mock[1]) + creation_events = self.safe_events_indexer.decode_elements(creation_events_mock) + setup_block = EthereumBlockFactory(block_hash=creation_events[2]["blockHash"]) + EthereumTxFactory( + tx_hash=creation_events[2]["transactionHash"], block=setup_block + ) + safe_creation_events = self.safe_events_indexer._get_safe_creation_events( + creation_events + ) + internal_txs = self.safe_events_indexer._process_safe_creation_events( + safe_creation_events, + ) + self.assertEqual(len(internal_txs), 1) + self.assertEqual(InternalTx.objects.count(), 3) + self.assertEqual(InternalTxDecoded.objects.count(), 1) + + # Process the same events shouldn't insert anything + internal_txs = self.safe_events_indexer._process_safe_creation_events( + safe_creation_events, + ) + self.assertEqual(len(internal_txs), 0) + self.assertEqual(InternalTx.objects.count(), 3) + self.assertEqual(InternalTxDecoded.objects.count(), 1) + class TestSafeEventsIndexerV1_3_0(TestSafeEventsIndexerV1_4_1): @property