diff --git a/safe_transaction_service/account_abstraction/filters.py b/safe_transaction_service/account_abstraction/filters.py new file mode 100644 index 000000000..6ab9fce95 --- /dev/null +++ b/safe_transaction_service/account_abstraction/filters.py @@ -0,0 +1,47 @@ +import django_filters +from django_filters import rest_framework as filters +from safe_eth.eth.django.filters import Keccak256Filter + +from safe_transaction_service.utils.filters import filter_overrides + +from .models import SafeOperation + + +class SafeOperationFilter(filters.FilterSet): + executed = django_filters.BooleanFilter(method="filter_executed") + has_confirmations = django_filters.BooleanFilter(method="filter_confirmations") + execution_date__gte = django_filters.IsoDateTimeFilter( + field_name="user_operation__ethereum_tx__block__timestamp", lookup_expr="gte" + ) + execution_date__lte = django_filters.IsoDateTimeFilter( + field_name="user_operation__ethereum_tx__block__timestamp", lookup_expr="lte" + ) + submission_date__gte = django_filters.IsoDateTimeFilter( + field_name="created", lookup_expr="gte" + ) + submission_date__lte = django_filters.IsoDateTimeFilter( + field_name="created", lookup_expr="lte" + ) + transaction_hash = Keccak256Filter(field_name="user_operation__ethereum_tx_id") + + def filter_confirmations(self, queryset, _name: str, value: bool): + if value: + return queryset.with_confirmations() + else: + return queryset.without_confirmations() + + def filter_executed(self, queryset, _name: str, value: bool): + if value: + return queryset.executed() + else: + return queryset.not_executed() + + class Meta: + model = SafeOperation + fields = { + "modified": ["lt", "gt", "lte", "gte"], + "valid_after": ["lt", "gt", "lte", "gte"], + "valid_until": ["lt", "gt", "lte", "gte"], + "module_address": ["exact"], + } + filter_overrides = filter_overrides diff --git a/safe_transaction_service/account_abstraction/models.py b/safe_transaction_service/account_abstraction/models.py index 3bcb5f9f3..1a16bce11 100644 --- a/safe_transaction_service/account_abstraction/models.py +++ b/safe_transaction_service/account_abstraction/models.py @@ -126,7 +126,22 @@ def __str__(self) -> str: return f"{HexBytes(self.user_operation_id).hex()} UserOperationReceipt" +class SafeOperationQuerySet(models.QuerySet): + def executed(self): + return self.exclude(user_operation__ethereum_tx=None) + + def not_executed(self): + return self.filter(user_operation__ethereum_tx=None) + + def with_confirmations(self): + return self.exclude(confirmations__isnull=True) + + def without_confirmations(self): + return self.filter(confirmations__isnull=True) + + class SafeOperation(TimeStampedModel): + objects = SafeOperationQuerySet.as_manager() hash = Keccak256Field(primary_key=True) # safeOperationHash user_operation = models.OneToOneField( UserOperation, on_delete=models.CASCADE, related_name="safe_operation" diff --git a/safe_transaction_service/account_abstraction/tests/test_views.py b/safe_transaction_service/account_abstraction/tests/test_views.py index f0faab2c9..1c5b03af9 100644 --- a/safe_transaction_service/account_abstraction/tests/test_views.py +++ b/safe_transaction_service/account_abstraction/tests/test_views.py @@ -175,6 +175,16 @@ def test_safe_operations_view(self): {"count": 1, "next": None, "previous": None, "results": [expected]}, ) + # Check confirmations flag + response = self.client.get( + reverse("v1:account_abstraction:safe-operations", args=(safe_address,)) + + "?has_confirmations=True" + ) + self.assertDictEqual( + response.json(), + {"count": 0, "next": None, "previous": None, "results": []}, + ) + # Add a confirmation safe_operation_confirmation = factories.SafeOperationConfirmationFactory( safe_operation=safe_operation @@ -204,6 +214,47 @@ def test_safe_operations_view(self): {"count": 1, "next": None, "previous": None, "results": [expected]}, ) + # Check executed flag + response = self.client.get( + reverse("v1:account_abstraction:safe-operations", args=(safe_address,)) + + "?executed=False" + ) + self.assertDictEqual( + response.json(), + {"count": 0, "next": None, "previous": None, "results": []}, + ) + + response = self.client.get( + reverse("v1:account_abstraction:safe-operations", args=(safe_address,)) + + "?executed=True" + ) + self.assertDictEqual( + response.json(), + {"count": 1, "next": None, "previous": None, "results": [expected]}, + ) + + # Set transaction as not executed and check again + safe_operation.user_operation.ethereum_tx = None + safe_operation.user_operation.save(update_fields=["ethereum_tx"]) + response = self.client.get( + reverse("v1:account_abstraction:safe-operations", args=(safe_address,)) + + "?executed=True" + ) + self.assertDictEqual( + response.json(), + {"count": 0, "next": None, "previous": None, "results": []}, + ) + + response = self.client.get( + reverse("v1:account_abstraction:safe-operations", args=(safe_address,)) + + "?executed=False" + ) + expected["userOperation"]["ethereumTxHash"] = None + self.assertDictEqual( + response.json(), + {"count": 1, "next": None, "previous": None, "results": [expected]}, + ) + @mock.patch.object( SafeOperationSerializer, "_get_owners", diff --git a/safe_transaction_service/account_abstraction/views.py b/safe_transaction_service/account_abstraction/views.py index 2599f718f..8d9b6f310 100644 --- a/safe_transaction_service/account_abstraction/views.py +++ b/safe_transaction_service/account_abstraction/views.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from safe_eth.eth.utils import fast_is_checksum_address -from . import pagination, serializers +from . import filters, pagination, serializers from .models import SafeOperation, SafeOperationConfirmation, UserOperation @@ -29,6 +29,7 @@ class SafeOperationsView(ListCreateAPIView): django_filters.rest_framework.DjangoFilterBackend, OrderingFilter, ] + filterset_class = filters.SafeOperationFilter ordering = ["-user_operation__nonce", "-created"] ordering_fields = ["user_operation__nonce", "created"] pagination_class = pagination.DefaultPagination diff --git a/safe_transaction_service/history/filters.py b/safe_transaction_service/history/filters.py index 6ab49da1b..8446fb594 100644 --- a/safe_transaction_service/history/filters.py +++ b/safe_transaction_service/history/filters.py @@ -4,19 +4,10 @@ from django_filters import rest_framework as filters from rest_framework.exceptions import ValidationError from safe_eth.eth.django.filters import EthereumAddressFilter, Keccak256Filter -from safe_eth.eth.django.models import ( - EthereumAddressBinaryField, - Keccak256Field, - Uint256Field, -) -from .models import ModuleTransaction, MultisigTransaction +from safe_transaction_service.utils.filters import filter_overrides -filter_overrides = { - Uint256Field: {"filter_class": django_filters.NumberFilter}, - Keccak256Field: {"filter_class": Keccak256Filter}, - EthereumAddressBinaryField: {"filter_class": EthereumAddressFilter}, -} +from .models import ModuleTransaction, MultisigTransaction class DelegateListFilter(filters.FilterSet): diff --git a/safe_transaction_service/utils/filters.py b/safe_transaction_service/utils/filters.py new file mode 100644 index 000000000..7f74d9b52 --- /dev/null +++ b/safe_transaction_service/utils/filters.py @@ -0,0 +1,13 @@ +import django_filters +from safe_eth.eth.django.filters import EthereumAddressFilter, Keccak256Filter +from safe_eth.eth.django.models import ( + EthereumAddressBinaryField, + Keccak256Field, + Uint256Field, +) + +filter_overrides = { + Uint256Field: {"filter_class": django_filters.NumberFilter}, + Keccak256Field: {"filter_class": Keccak256Filter}, + EthereumAddressBinaryField: {"filter_class": EthereumAddressFilter}, +}