diff --git a/safe_transaction_service/history/models.py b/safe_transaction_service/history/models.py index 26b674634..2b9a64113 100644 --- a/safe_transaction_service/history/models.py +++ b/safe_transaction_service/history/models.py @@ -2019,6 +2019,7 @@ class WebHookType(Enum): OUTGOING_TOKEN = 9 MESSAGE_CREATED = 10 MESSAGE_CONFIRMATION = 11 + DELETED_MULTISIG_TRANSACTION = 12 class WebHookQuerySet(models.QuerySet): diff --git a/safe_transaction_service/history/services/webhooks.py b/safe_transaction_service/history/services/webhooks.py index 49022168d..173f862b2 100644 --- a/safe_transaction_service/history/services/webhooks.py +++ b/safe_transaction_service/history/services/webhooks.py @@ -30,10 +30,12 @@ def build_webhook_payload( instance: Union[ TokenTransfer, InternalTx, MultisigConfirmation, MultisigTransaction ], + deleted: bool = False, ) -> List[Dict[str, Any]]: """ :param sender: Sender type :param instance: Sender instance + :param deleted: If the instance has been deleted :return: A list of webhooks generated from the instance provided """ payloads: List[Dict[str, Any]] = [] @@ -48,6 +50,14 @@ def build_webhook_payload( ).hex(), } ] + elif sender == MultisigTransaction and deleted: + payloads = [ + { + "address": instance.safe, + "type": WebHookType.DELETED_MULTISIG_TRANSACTION.name, + "safeTxHash": HexBytes(instance.safe_tx_hash).hex(), + } + ] elif sender == MultisigTransaction: payload = { "address": instance.safe, diff --git a/safe_transaction_service/history/signals.py b/safe_transaction_service/history/signals.py index 893912a25..d91922568 100644 --- a/safe_transaction_service/history/signals.py +++ b/safe_transaction_service/history/signals.py @@ -2,7 +2,7 @@ from typing import Type, Union from django.db.models import Model -from django.db.models.signals import post_save +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from django.utils import timezone @@ -112,30 +112,7 @@ def safe_master_copy_clear_cache( SafeMasterCopy.objects.get_version_for_address.cache_clear() -@receiver( - post_save, - sender=ModuleTransaction, - dispatch_uid="module_transaction.process_webhook", -) -@receiver( - post_save, - sender=MultisigConfirmation, - dispatch_uid="multisig_confirmation.process_webhook", -) -@receiver( - post_save, - sender=MultisigTransaction, - dispatch_uid="multisig_transaction.process_webhook", -) -@receiver( - post_save, sender=ERC20Transfer, dispatch_uid="erc20_transfer.process_webhook" -) -@receiver( - post_save, sender=ERC721Transfer, dispatch_uid="erc721_transfer.process_webhook" -) -@receiver(post_save, sender=InternalTx, dispatch_uid="internal_tx.process_webhook") -@receiver(post_save, sender=SafeContract, dispatch_uid="safe_contract.process_webhook") -def process_webhook( +def _process_webhook( sender: Type[Model], instance: Union[ TokenTransfer, @@ -145,10 +122,14 @@ def process_webhook( SafeContract, ], created: bool, - **kwargs, -) -> None: + deleted: bool, +): + assert not ( + created and deleted + ), "An instance cannot be created and deleted at the same time" + logger.debug("Start building payloads for created=%s object=%s", created, instance) - payloads = build_webhook_payload(sender, instance) + payloads = build_webhook_payload(sender, instance, deleted=deleted) logger.debug( "End building payloads %s for created=%s object=%s", payloads, created, instance ) @@ -177,6 +158,55 @@ def process_webhook( ) +@receiver( + post_save, + sender=ModuleTransaction, + dispatch_uid="module_transaction.process_webhook", +) +@receiver( + post_save, + sender=MultisigConfirmation, + dispatch_uid="multisig_confirmation.process_webhook", +) +@receiver( + post_save, + sender=MultisigTransaction, + dispatch_uid="multisig_transaction.process_webhook", +) +@receiver( + post_save, sender=ERC20Transfer, dispatch_uid="erc20_transfer.process_webhook" +) +@receiver( + post_save, sender=ERC721Transfer, dispatch_uid="erc721_transfer.process_webhook" +) +@receiver(post_save, sender=InternalTx, dispatch_uid="internal_tx.process_webhook") +@receiver(post_save, sender=SafeContract, dispatch_uid="safe_contract.process_webhook") +def process_webhook( + sender: Type[Model], + instance: Union[ + TokenTransfer, + InternalTx, + MultisigConfirmation, + MultisigTransaction, + SafeContract, + ], + created: bool, + **kwargs, +) -> None: + return _process_webhook(sender, instance, created, False) + + +@receiver( + post_delete, + sender=MultisigTransaction, + dispatch_uid="multisig_transaction.process_delete_webhook", +) +def process_delete_webhook( + sender: Type[Model], instance: MultisigTransaction, *args, **kwargs +): + return _process_webhook(sender, instance, False, True) + + @receiver( post_save, sender=SafeLastStatus, diff --git a/safe_transaction_service/history/tests/test_signals.py b/safe_transaction_service/history/tests/test_signals.py index 2c90826f3..9e5ca40b3 100644 --- a/safe_transaction_service/history/tests/test_signals.py +++ b/safe_transaction_service/history/tests/test_signals.py @@ -1,5 +1,6 @@ from datetime import timedelta from unittest import mock +from unittest.mock import MagicMock from django.db.models.signals import post_save from django.test import TestCase @@ -83,6 +84,14 @@ def test_build_webhook_payload(self): self.assertEqual(payload["type"], WebHookType.PENDING_MULTISIG_TRANSACTION.name) self.assertEqual(payload["chainId"], str(EthereumNetwork.GANACHE.value)) + payload = build_webhook_payload( + MultisigTransaction, + MultisigTransactionFactory(ethereum_tx=None), + deleted=True, + )[0] + self.assertEqual(payload["type"], WebHookType.DELETED_MULTISIG_TRANSACTION.name) + self.assertEqual(payload["chainId"], str(EthereumNetwork.GANACHE.value)) + safe_address = self.deploy_test_safe().address safe_message = SafeMessageFactory(safe=safe_address) payload = build_webhook_payload(SafeMessage, safe_message)[0] @@ -166,3 +175,52 @@ def test_is_relevant_notification_multisig_transaction(self): self.assertFalse( is_relevant_notification(multisig_tx.__class__, multisig_tx, created=False) ) + + @mock.patch.object(send_webhook_task, "apply_async") + @mock.patch.object(send_event_to_queue_task, "delay") + def test_signals_are_correctly_fired( + self, + send_event_to_queue_task_mock: MagicMock, + webhook_task_mock: MagicMock, + ): + # Not trusted txs should not fire any event + MultisigTransactionFactory(trusted=False) + webhook_task_mock.assert_not_called() + send_event_to_queue_task_mock.assert_not_called() + + # Trusted txs should fire an event + multisig_tx: MultisigTransaction = MultisigTransactionFactory(trusted=True) + pending_multisig_transaction_payload = { + "address": multisig_tx.safe, + "safeTxHash": multisig_tx.safe_tx_hash, + "type": WebHookType.EXECUTED_MULTISIG_TRANSACTION.name, + "failed": "false", + "txHash": multisig_tx.ethereum_tx_id, + "chainId": str(EthereumNetwork.GANACHE.value), + } + webhook_task_mock.assert_called_with( + args=(multisig_tx.safe, pending_multisig_transaction_payload), priority=2 + ) + send_event_to_queue_task_mock.assert_called_with( + pending_multisig_transaction_payload + ) + + # Deleting a tx should fire an event + webhook_task_mock.reset_mock() + send_event_to_queue_task_mock.reset_mock() + safe_tx_hash = multisig_tx.safe_tx_hash + multisig_tx.delete() + + deleted_multisig_transaction_payload = { + "address": multisig_tx.safe, + "safeTxHash": safe_tx_hash, + "type": WebHookType.DELETED_MULTISIG_TRANSACTION.name, + "chainId": str(EthereumNetwork.GANACHE.value), + } + + webhook_task_mock.assert_called_with( + args=(multisig_tx.safe, deleted_multisig_transaction_payload), priority=2 + ) + send_event_to_queue_task_mock.assert_called_with( + deleted_multisig_transaction_payload + ) diff --git a/safe_transaction_service/safe_messages/tests/test_signals.py b/safe_transaction_service/safe_messages/tests/test_signals.py index f4351f94b..2ddbc638e 100644 --- a/safe_transaction_service/safe_messages/tests/test_signals.py +++ b/safe_transaction_service/safe_messages/tests/test_signals.py @@ -1,4 +1,5 @@ from unittest import mock +from unittest.mock import MagicMock from django.db.models.signals import post_save from django.test import TestCase @@ -28,8 +29,8 @@ class TestSafeMessageSignals(SafeTestCaseMixin, TestCase): @mock.patch.object(send_event_to_queue_task, "delay") def test_process_webhook( self, - send_event_to_queue_task_mock, - webhook_task_mock, + send_event_to_queue_task_mock: MagicMock, + webhook_task_mock: MagicMock, ): safe_address = self.deploy_test_safe().address safe_message = SafeMessageFactory(safe=safe_address) @@ -64,8 +65,8 @@ def test_process_webhook( @mock.patch.object(send_event_to_queue_task, "delay") def test_signals_are_correctly_fired( self, - send_event_to_queue_task_mock, - webhook_task_mock, + send_event_to_queue_task_mock: MagicMock, + webhook_task_mock: MagicMock, ): safe_address = self.deploy_test_safe().address # Create a confirmation should fire a signal and webhooks should be sended