From 65f1eede0e2acbf08d7fd7a92f3526240c180a68 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Wed, 27 Sep 2023 14:06:37 +0330 Subject: [PATCH] Support multiple wallet for same chains --- ...r_wallet_unique_together_wallet_primary.py | 22 +++ .../0019_alter_wallet_unique_together.py | 17 +++ authentication/models.py | 12 +- authentication/permissions.py | 9 ++ authentication/serializers.py | 49 ++++--- authentication/tests.py | 85 ++++++------ authentication/urls.py | 22 +-- authentication/views.py | 125 +++--------------- faucet/views.py | 5 +- 9 files changed, 158 insertions(+), 188 deletions(-) create mode 100644 authentication/migrations/0018_alter_wallet_unique_together_wallet_primary.py create mode 100644 authentication/migrations/0019_alter_wallet_unique_together.py diff --git a/authentication/migrations/0018_alter_wallet_unique_together_wallet_primary.py b/authentication/migrations/0018_alter_wallet_unique_together_wallet_primary.py new file mode 100644 index 00000000..19b2de1e --- /dev/null +++ b/authentication/migrations/0018_alter_wallet_unique_together_wallet_primary.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.4 on 2023-09-27 11:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0017_alter_userprofile_username'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='wallet', + unique_together={('wallet_type', 'address')}, + ), + migrations.AddField( + model_name='wallet', + name='primary', + field=models.BooleanField(default=False), + ), + ] diff --git a/authentication/migrations/0019_alter_wallet_unique_together.py b/authentication/migrations/0019_alter_wallet_unique_together.py new file mode 100644 index 00000000..599d3da1 --- /dev/null +++ b/authentication/migrations/0019_alter_wallet_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.4 on 2023-09-28 03:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0018_alter_wallet_unique_together_wallet_primary'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='wallet', + unique_together=set(), + ), + ] diff --git a/authentication/models.py b/authentication/models.py index d8ee7cdb..2c7f2be9 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -19,6 +19,14 @@ def get_or_create(self, first_context_id): return _profile +class WalletManager(models.Manager): + def get_primary_wallet(self): + try: + self.get(primary=True, wallet_type='EVM') + except Wallet.DoesNotExist: + return None + + class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.PROTECT, related_name="profile") initial_context_id = models.CharField(max_length=512, unique=True) @@ -107,9 +115,9 @@ class Wallet(models.Model): UserProfile, on_delete=models.PROTECT, related_name="wallets" ) address = models.CharField(max_length=512, unique=True) + primary = models.BooleanField(default=False, null=False, blank=False) - class Meta: - unique_together = (("wallet_type", "user_profile"),) + objects = WalletManager() def __str__(self): return f"{self.wallet_type} Wallet for profile with contextId {self.user_profile.initial_context_id}" diff --git a/authentication/permissions.py b/authentication/permissions.py index c871a94c..15862660 100644 --- a/authentication/permissions.py +++ b/authentication/permissions.py @@ -17,3 +17,12 @@ class IsAuraVerified(BasePermission): def has_permission(self, request, view): return bool(request.user.profile.is_aura_verified) + + +class IsOwner(BasePermission): + """ + Just owner has can access + """ + + def has_object_permission(self, request, view, obj): + return obj.user_profile == request.user.profile diff --git a/authentication/serializers.py b/authentication/serializers.py index b3359f37..fa7d5940 100644 --- a/authentication/serializers.py +++ b/authentication/serializers.py @@ -1,12 +1,11 @@ -from django.db import IntegrityError +from rest_framework.authtoken.models import Token +from rest_framework import serializers + from authentication.models import ( UserProfile, Wallet, ) -from rest_framework.authtoken.models import Token -from rest_framework import serializers from faucet.faucet_manager.claim_manager import LimitedChainClaimManager - from faucet.models import GlobalSettings @@ -28,23 +27,6 @@ def update(self, instance, validated_data): pass -# class SetUsernameSerializer(serializers.Serializer): -# username = UsernameRequestSerializer.username - -# def save(self, user_profile): -# username = self.validated_data.get("username") - -# try: -# user_profile.username = username -# user_profile.save() -# return {"message": "Username Set"} - -# except IntegrityError: -# raise ValidationError( -# {"message": "This username already exists. Try another one."} -# ) - - class WalletSerializer(serializers.ModelSerializer): class Meta: model = Wallet @@ -52,8 +34,24 @@ class Meta: "pk", "wallet_type", "address", + 'primary' ] + def update(self, instance, validated_data): + if validated_data.get('primary') is False or instance.wallet_type != 'EVM': + raise serializers.ValidationError({'message': 'primary must be true or wallet_type must be EVM'}) + user_profile = self.context["request"].user.profile + try: + wallet = Wallet.objects.get(user_profile=user_profile, primary=True) + wallet.primary = False + instance.primary = True + Wallet.objects.bulk_update([wallet, instance], ['primary']) + return instance + except Wallet.DoesNotExist: + instance.primary = True + instance.save() + return instance + class ProfileSerializer(serializers.ModelSerializer): wallets = WalletSerializer(many=True, read_only=True) @@ -81,10 +79,11 @@ def get_total_weekly_claims_remaining(self, instance): gs = GlobalSettings.objects.first() if gs is not None: return ( - gs.weekly_chain_claim_limit - - LimitedChainClaimManager.get_total_weekly_claims(instance) + gs.weekly_chain_claim_limit + - LimitedChainClaimManager.get_total_weekly_claims(instance) ) - + + class SimpleProfilerSerializer(serializers.ModelSerializer): wallets = WalletSerializer(many=True, read_only=True) username = serializers.SerializerMethodField() @@ -102,4 +101,4 @@ class Meta: def get_username(self, user_profile: UserProfile): if not user_profile.username: return f"User{user_profile.pk}" - return user_profile.username \ No newline at end of file + return user_profile.username diff --git a/authentication/tests.py b/authentication/tests.py index c42a414a..3fb7a8a3 100644 --- a/authentication/tests.py +++ b/authentication/tests.py @@ -3,10 +3,12 @@ from django.utils import timezone from django.urls import reverse from django.contrib.auth.models import User +from rest_framework.response import Response from rest_framework.test import APITestCase from rest_framework.authtoken.models import Token -from rest_framework.status import HTTP_403_FORBIDDEN, HTTP_409_CONFLICT, HTTP_200_OK -from authentication.models import UserProfile +from rest_framework.status import HTTP_403_FORBIDDEN, HTTP_409_CONFLICT, HTTP_200_OK, HTTP_400_BAD_REQUEST, \ + HTTP_404_NOT_FOUND, HTTP_201_CREATED +from authentication.models import UserProfile, Wallet from faucet.models import ClaimReceipt ### get address as username and signed address as password and verify signature @@ -30,7 +32,7 @@ lambda a, b: True, ) def create_new_user( - _address="0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9", + _address="0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9", ) -> UserProfile: # (u, created) = User.objects.get_or_create(username=_address, password="test") p = UserProfile.objects.get_or_create(_address) @@ -50,6 +52,11 @@ def create_verified_user() -> UserProfile: return user +def create_new_wallet(user_profile, _address, wallet_type) -> Wallet: + wallet, is_create = Wallet.objects.get_or_create(user_profile=user_profile, address=_address, + wallet_type=wallet_type) + return wallet + class CheckUsernameTestCase(APITestCase): def setUp(self) -> None: self.endpoint = "AUTHENTICATION:check-username" @@ -219,75 +226,75 @@ def test_become_sponsor(self): self.assertEqual(response.status_code, HTTP_200_OK) -class TestSetWalletAddress(APITestCase): +class TestListCreateWallet(APITestCase): def setUp(self) -> None: self.password = "test" self._address = "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461G3Ef9A9" - self.endpoint = reverse("AUTHENTICATION:set-wallet-user") + self.endpoint = reverse("AUTHENTICATION:wallets-user") self.user_profile = create_new_user() self.client.force_authenticate(user=self.user_profile.user) def test_invalid_arguments_provided_should_fail(self): response = self.client.post(self.endpoint) - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) response = self.client.post(self.endpoint, data={"address": False}) - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) response = self.client.post(self.endpoint, data={"wallet_type": False}) - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + + def test_create_wallet_address(self): + response = self.client.post(self.endpoint, + data={"address": self._address, "wallet_type": "EVM", "primary": True}) + self.assertEqual(response.status_code, HTTP_201_CREATED) - def test_set_same_address_for_multiple_users_should_fail(self): + def test_create_same_address_twice(self): response = self.client.post( - self.endpoint, data={"address": self._address, "wallet_type": "EVM"} + self.endpoint, data={"address": self._address, "wallet_type": "EVM", 'primary': True} ) - self.assertEqual(response.status_code, HTTP_200_OK) - + self.assertEqual(response.status_code, HTTP_201_CREATED) response = self.client.post( - self.endpoint, data={"address": self._address, "wallet_type": "Solana"} + self.endpoint, data={"address": self._address, "wallet_type": "EVM", 'primary': True} ) - self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) - def test_not_existing_wallet_then_create_and_set_address_for_that_is_ok(self): + def test_get_wallet_list(self): response = self.client.post( - self.endpoint, data={"address": self._address, "wallet_type": "EVM"} + self.endpoint, data={"address": self._address, "wallet_type": "EVM", 'primary': True} ) + self.assertEqual(response.status_code, HTTP_201_CREATED) + response = self.client.get(self.endpoint, {'wallet_type': 'EVM'}) self.assertEqual(response.status_code, HTTP_200_OK) + self.assertGreater(len(response.data), 0) -# class TestGetWalletAddress(APITestCase): -# def setUp(self) -> None: -# self.password = "test" -# self._address = "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9" -# self.endpoint_set = reverse('AUTHENTICATION:set-wallet-user') -# self.endpoint_get = reverse('AUTHENTICATION:get-wallet-user') -# self.user_profile = create_new_user() -# self.client.force_authenticate(user=self.user_profile.user) -# -# def test_get_existing_wallet_is_ok(self): -# response = self.client.post(self.endpoint_set, data={'address': self._address, 'wallet_type': "EVM"}) -# self.assertEqual(response.status_code, HTTP_200_OK) -# -# response = self.client.post(self.endpoint_get, data={'wallet_type': "EVM"}) -# self.assertEqual(response.status_code, HTTP_200_OK) -# -# def test_not_existing_wallet_should_fail_getting_profile(self): -# response = self.client.post(self.endpoint_get, data={'wallet_type': "EVM"}) -# self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) - - -class TestGetWalletsView(APITestCase): +class TestWalletView(APITestCase): def setUp(self) -> None: self.password = "test" self._address = "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9" - self.endpoint = reverse("AUTHENTICATION:get-wallets-user") self.user_profile = create_new_user() + wallet = create_new_wallet(self.user_profile, self._address, 'EVM') + self.endpoint = reverse("AUTHENTICATION:wallet-user", kwargs={'pk': wallet.pk}) self.client.force_authenticate(user=self.user_profile.user) def test_request_to_this_api_is_ok(self): response = self.client.get(self.endpoint) self.assertEqual(response.status_code, HTTP_200_OK) + def test_change_primary_ture(self): + response: Response = self.client.patch(self.endpoint, data={'primary': True}) + self.assertEqual(response.status_code, HTTP_200_OK) + self.assertEqual(response.data.get('primary'), True) + + def test_access_to_another_user_wallet(self): + _address = '0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A2' + other_user = create_new_user(_address) + wallet = create_new_wallet(other_user, _address, 'EVM') + _endpoint = reverse('AUTHENTICATION:wallet-user', kwargs={'pk': wallet.pk}) + response = self.client.get(_endpoint) + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) + class TestGetProfileView(APITestCase): def setUp(self) -> None: diff --git a/authentication/urls.py b/authentication/urls.py index 3ec01b5e..59ff19cd 100644 --- a/authentication/urls.py +++ b/authentication/urls.py @@ -17,24 +17,14 @@ name="check-username", ), path( - "user/set-wallet/", - SetWalletAddressView.as_view(), - name="set-wallet-user", + "user/wallets/", + WalletListCreateView.as_view(), + name="wallets-user", ), path( - "user/get-wallet/", - GetWalletAddressView.as_view(), - name="get-wallet-user", - ), - path( - "user/delete-wallet/", - DeleteWalletAddressView.as_view(), - name="delete-wallet-user", - ), - path( - "user/get-wallets/", - GetWalletsView.as_view(), - name="get-wallets-user", + "user/wallets//", + WalletView.as_view(), + name="wallet-user", ), path("user/info/", GetProfileView.as_view(), name="get-profile-user"), path("user/sponsor/", SponsorView.as_view(), name="sponsor-user"), diff --git a/authentication/views.py b/authentication/views.py index 4c668f91..6aa98a57 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -1,12 +1,17 @@ import time from django.db import IntegrityError +from django_filters.rest_framework import DjangoFilterBackend from rest_framework.permissions import IsAuthenticated -from rest_framework.generics import CreateAPIView, RetrieveAPIView, ListAPIView +from rest_framework.generics import CreateAPIView, RetrieveAPIView, ListAPIView, RetrieveUpdateAPIView, \ + ListCreateAPIView +from rest_framework.status import HTTP_409_CONFLICT + from authentication.models import UserProfile, Wallet from rest_framework.authtoken.models import Token from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.exceptions import ValidationError from drf_yasg.utils import swagger_auto_schema from authentication.helpers import ( BRIGHTID_SOULDBOUND_INTERFACE, @@ -14,12 +19,15 @@ is_username_valid_and_available, ) from drf_yasg import openapi + +from authentication.permissions import IsOwner from authentication.serializers import ( UsernameRequestSerializer, MessageResponseSerializer, ProfileSerializer, WalletSerializer, ) +from core.filters import IsOwnerFilterBackend class UserProfileCountView(ListAPIView): @@ -245,119 +253,28 @@ def post(self, request, *args, **kwargs): return Response(request_serializer.errors, status=400) -class SetWalletAddressView(CreateAPIView): +class WalletListCreateView(ListCreateAPIView): + queryset = Wallet.objects.all() permission_classes = [IsAuthenticated] + serializer_class = WalletSerializer + filter_backends = [IsOwnerFilterBackend, DjangoFilterBackend] + filterset_fields = ['wallet_type'] - def post(self, request, *args, **kwargs): - address = request.data.get("address", None) - wallet_type = request.data.get("wallet_type", None) - if not address or not wallet_type: - return Response({"message": "Invalid request"}, status=403) - - user_profile = request.user.profile - - try: - w = Wallet.objects.get(user_profile=user_profile, wallet_type=wallet_type) - w.address = address - w.save() - - return Response( - {"message": f"{wallet_type} wallet address updated"}, status=200 - ) - - except Wallet.DoesNotExist: - try: - Wallet.objects.create( - user_profile=user_profile, wallet_type=wallet_type, address=address - ) - return Response( - {"message": f"{wallet_type} wallet address set"}, status=200 - ) - # catch unique constraint error - except IntegrityError: - return Response( - { - "message": f"{wallet_type} wallet address is not unique. use another address" - }, - status=403, - ) - - -class GetWalletAddressView(RetrieveAPIView): - permission_classes = [IsAuthenticated] - - def get(self, request, *args, **kwargs): - wallet_type = request.data.get("wallet_type", None) - if not wallet_type: - return Response({"message": "Invalid request"}, status=403) - - # get user profile - user_profile = request.user.profile - - try: - # check if wallet already exists - wallet = Wallet.objects.get( - user_profile=user_profile, wallet_type=wallet_type - ) - return Response({"address": wallet.address}, status=200) - - except Wallet.DoesNotExist: - return Response( - {"message": f"{wallet_type} wallet address not set"}, status=403 - ) - - -class DeleteWalletAddressView(RetrieveAPIView): - permission_classes = [IsAuthenticated] - - def get(self, request, *args, **kwargs): - wallet_type = request.data.get("wallet_type", None) - if not wallet_type: - return Response({"message": "Invalid request"}, status=403) - - # get user profile - user_profile = request.user.profile + def perform_create(self, serializer): + serializer.save(user_profile=self.request.user.profile) - try: - # check if wallet already exists - wallet = Wallet.objects.get( - user_profile=user_profile, wallet_type=wallet_type - ) - wallet.delete() - return Response( - {"message": f"{wallet_type} wallet address deleted"}, status=200 - ) - - except Wallet.DoesNotExist: - return Response( - {"message": f"{wallet_type} wallet address not set"}, status=403 - ) - -class GetWalletsView(ListAPIView): - permission_classes = [IsAuthenticated] +class WalletView(RetrieveUpdateAPIView): + permission_classes = [IsAuthenticated, IsOwner] serializer_class = WalletSerializer - - def get_queryset(self): - return Wallet.objects.filter(user_profile=self.request.user.profile) + queryset = Wallet.objects.all() + filter_backends = [IsOwnerFilterBackend] + http_method_names = ['get', 'patch'] class GetProfileView(RetrieveAPIView): permission_classes = [IsAuthenticated] serializer_class = ProfileSerializer - # def get(self, request, *args, **kwargs): - # user = request.user - - # token, bol = Token.objects.get_or_create(user=user) - # print("token", token) - - # # return Response({"token": token.key}, status=200) - # # return token and profile using profile serializer for profile - # return Response( - # {"token": token.key, "profile": ProfileSerializer(user.profile).data}, - # status=200, - # ) - def get_object(self): return self.request.user.profile diff --git a/faucet/views.py b/faucet/views.py index 8548dc7a..dcf044c6 100644 --- a/faucet/views.py +++ b/faucet/views.py @@ -258,7 +258,7 @@ def get_object(self): user_rank = queryset.filter(sum_total_price__gt=user_obj.get('sum_total_price')).count() + 1 user_obj['rank'] = user_rank user_obj['username'] = self.get_user().username - user_obj['wallet'] = self.get_user().wallets.all()[0].address + user_obj['wallet'] = self.get_user().wallets.get_primary_wallet() interacted_chains = list(DonationReceipt.objects.filter( user_profile=self.get_user()).filter(status=ClaimReceipt.VERIFIED).values_list( 'chain', flat=True).distinct()) @@ -283,7 +283,8 @@ def list(self, request, *args, **kwargs): 'chain', flat=True).distinct() queryset = donation_receipt.annotate(interacted_chains=ArraySubquery(subquery_interacted_chains)) subquery_username = UserProfile.objects.filter(pk=OuterRef('user_profile')).values('username') - subquery_wallet = Wallet.objects.filter(user_profile=OuterRef('user_profile')).values('address') + subquery_wallet = Wallet.objects.filter(user_profile=OuterRef('user_profile'), primary=True, + wallet_type='EVM').values('address') queryset = queryset.annotate(username=Subquery(subquery_username), wallet=Subquery(subquery_wallet)) page = self.paginate_queryset(queryset) if page is not None: