From 44a92d0891d9a8d0f06eb3dc6cbdb2c2a74f2859 Mon Sep 17 00:00:00 2001 From: Shayan Shiravani Date: Wed, 10 Jan 2024 19:23:44 +0330 Subject: [PATCH] Token contribution hub --- ...rmissions_tokendistribution_constraints.py | 18 +++++ ...29_tokendistribution_email_url_and_more.py | 51 +++++++++++++ ...stribution_distributor_address_and_more.py | 27 +++++++ tokenTap/models.py | 23 +++++- tokenTap/serializers.py | 71 +++++++++++++++++-- tokenTap/tests.py | 53 +++++++++++--- tokenTap/views.py | 19 ++++- 7 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 tokenTap/migrations/0028_rename_permissions_tokendistribution_constraints.py create mode 100644 tokenTap/migrations/0029_tokendistribution_email_url_and_more.py create mode 100644 tokenTap/migrations/0030_tokendistribution_distributor_address_and_more.py diff --git a/tokenTap/migrations/0028_rename_permissions_tokendistribution_constraints.py b/tokenTap/migrations/0028_rename_permissions_tokendistribution_constraints.py new file mode 100644 index 00000000..628a196d --- /dev/null +++ b/tokenTap/migrations/0028_rename_permissions_tokendistribution_constraints.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2024-01-09 19:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0027_merge_20240105_1916'), + ] + + operations = [ + migrations.RenameField( + model_name='tokendistribution', + old_name='permissions', + new_name='constraints', + ), + ] diff --git a/tokenTap/migrations/0029_tokendistribution_email_url_and_more.py b/tokenTap/migrations/0029_tokendistribution_email_url_and_more.py new file mode 100644 index 00000000..fe1d0f95 --- /dev/null +++ b/tokenTap/migrations/0029_tokendistribution_email_url_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.0.4 on 2024-01-10 13:59 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0028_rename_permissions_tokendistribution_constraints'), + ] + + operations = [ + migrations.AddField( + model_name='tokendistribution', + name='email_url', + field=models.EmailField(default='', max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name='tokendistribution', + name='necessary_information', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='tokendistribution', + name='rejection_reason', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='tokendistribution', + name='start_at', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='tokendistribution', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('REJECTED', 'Rejected'), ('VERIFIED', 'Verified')], default='PENDING', max_length=10), + ), + migrations.AddField( + model_name='tokendistribution', + name='telegram_url', + field=models.URLField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='tokendistribution', + name='deadline', + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/tokenTap/migrations/0030_tokendistribution_distributor_address_and_more.py b/tokenTap/migrations/0030_tokendistribution_distributor_address_and_more.py new file mode 100644 index 00000000..d113476a --- /dev/null +++ b/tokenTap/migrations/0030_tokendistribution_distributor_address_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.4 on 2024-01-10 14:46 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0028_remove_wallet_unique_wallet_address_and_more'), + ('tokenTap', '0029_tokendistribution_email_url_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='tokendistribution', + name='distributor_address', + field=models.CharField(default='', max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name='tokendistribution', + name='distributor_profile', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.DO_NOTHING, related_name='token_distributions', to='authentication.userprofile'), + preserve_default=False, + ), + ] diff --git a/tokenTap/models.py b/tokenTap/models.py index 8816dcc7..91b3cd08 100644 --- a/tokenTap/models.py +++ b/tokenTap/models.py @@ -1,6 +1,7 @@ from django.core.cache import cache from django.db import models from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from authentication.models import UserProfile from core.models import Chain, UserConstraint @@ -24,12 +25,23 @@ class Constraint(UserConstraint): class TokenDistribution(models.Model): + class Status(models.TextChoices): + PENDING = "PENDING", _("Pending") + REJECTED = "REJECTED", _("Rejected") + VERIFIED = "VERIFIED", _("Verified") + name = models.CharField(max_length=100) distributor = models.CharField(max_length=100, null=True, blank=True) + distributor_profile = models.ForeignKey( + UserProfile, on_delete=models.DO_NOTHING, related_name="token_distributions" + ) + distributor_address = models.CharField(max_length=255) distributor_url = models.URLField(max_length=255, null=True, blank=True) discord_url = models.URLField(max_length=255, null=True, blank=True) twitter_url = models.URLField(max_length=255, null=True, blank=True) + email_url = models.EmailField(max_length=255) + telegram_url = models.URLField(max_length=255, null=True, blank=True) image_url = models.URLField(max_length=255, null=True, blank=True) token_image_url = models.URLField(max_length=255, null=True, blank=True) @@ -41,14 +53,21 @@ class TokenDistribution(models.Model): ) contract = models.CharField(max_length=255, null=True, blank=True) - permissions = models.ManyToManyField(Constraint, blank=True) + constraints = models.ManyToManyField(Constraint, blank=True) created_at = models.DateTimeField(auto_now_add=True) - deadline = models.DateTimeField(null=True, blank=True) + start_at = models.DateTimeField(default=timezone.now) + deadline = models.DateTimeField() max_number_of_claims = models.IntegerField(null=True, blank=True) notes = models.TextField(null=True, blank=True) + necessary_information = models.TextField(null=True, blank=True) + + status = models.CharField( + max_length=10, choices=Status.choices, default=Status.PENDING + ) + rejection_reason = models.TextField(null=True, blank=True) is_active = models.BooleanField(default=True) diff --git a/tokenTap/serializers.py b/tokenTap/serializers.py index 7c892e63..70b8f117 100644 --- a/tokenTap/serializers.py +++ b/tokenTap/serializers.py @@ -1,3 +1,6 @@ +import base64 +import json + from rest_framework import serializers from core.constraints import ConstraintVerification, get_constraint @@ -9,6 +12,8 @@ UserConstraint, ) +from .constants import CONTRACT_ADDRESSES + class ConstraintSerializer(UserConstraintBaseSerializer, serializers.ModelSerializer): class Meta(UserConstraintBaseSerializer.Meta): @@ -32,7 +37,7 @@ def update(self, instance, validated_data): class TokenDistributionSerializer(serializers.ModelSerializer): chain = ChainSerializer() - permissions = ConstraintSerializer(many=True) + constraints = ConstraintSerializer(many=True) class Meta: model = TokenDistribution @@ -43,6 +48,8 @@ class Meta: "distributor_url", "discord_url", "twitter_url", + "email_url", + "telegram_url", "image_url", "token_image_url", "token", @@ -50,13 +57,18 @@ class Meta: "amount", "chain", "contract", - "permissions", + "constraints", "created_at", + "start_at", "deadline", "max_number_of_claims", "number_of_claims", "total_claims_since_last_round", "notes", + "necessary_information", + "status", + "rejection_reason", + "is_active", "is_expired", "is_maxed_out", "is_claimable", @@ -65,7 +77,7 @@ class Meta: class SmallTokenDistributionSerializer(serializers.ModelSerializer): chain = ChainSerializer() - permissions = ConstraintSerializer(many=True) + constraints = ConstraintSerializer(many=True) class Meta: model = TokenDistribution @@ -82,7 +94,7 @@ class Meta: "amount", "chain", "contract", - "permissions", + "constraints", "created_at", "deadline", "max_number_of_claims", @@ -121,3 +133,54 @@ def get_payload(self, obj): class TokenDistributionClaimResponseSerializer(serializers.Serializer): detail = serializers.CharField() signature = TokenDistributionClaimSerializer() + + +class CreateTokenDistributionSerializer(serializers.ModelSerializer): + class Meta: + model = TokenDistribution + fields = "__all__" + + read_only_fields = [ + "pk", + "distributor_profile", + "created_at", + "status", + "rejection_reason", + "is_active", + ] + + def validate(self, data): + constraints = data["constraints"] + constraint_params = json.loads(base64.b64decode(data["constraint_params"])) + data["constraint_params"] = base64.b64decode(data["constraint_params"]).decode( + "utf-8" + ) + reversed_constraints = [] + if "reversed_constraints" in data: + reversed_constraints = str(data["reversed_constraints"]).split(",") + if len(constraints) != 0: + for c in constraints: + constraint_class: ConstraintVerification = get_constraint(c.name) + try: + if len(constraint_class.param_keys()) != 0: + constraint_class.is_valid_param_keys(constraint_params[c.name]) + except KeyError as e: + raise serializers.ValidationError( + {"constraint_params": [{f"{c.name}": str(e)}]} + ) + valid_constraints = [str(c.pk) for c in constraints] + if len(reversed_constraints) > 0: + for c in reversed_constraints: + if c not in valid_constraints: + raise serializers.ValidationError( + {"reversed_constraints": [{f"{c}": "Invalid constraint pk"}]} + ) + valid_chains = list(CONTRACT_ADDRESSES.keys()) + chain_id = data["chain"].chain_id + if chain_id not in valid_chains: + raise serializers.ValidationError({"chain": "Invalid value"}) + valid_contracts = list(CONTRACT_ADDRESSES[chain_id].values()) + if data["contract"] not in valid_contracts: + raise serializers.ValidationError({"contract": "Invalid value"}) + data["distributor_profile"] = self.context["user_profile"] + return data diff --git a/tokenTap/tests.py b/tokenTap/tests.py index 01f3ece8..a453f17c 100644 --- a/tokenTap/tests.py +++ b/tokenTap/tests.py @@ -22,6 +22,12 @@ class TokenDistributionTestCase(APITestCase): def setUp(self): + self.user_profile = UserProfile.objects.create( + user=User.objects.create_user(username="test", password="1234"), + initial_context_id="test", + username="test", + ) + self.chain = Chain.objects.create( chain_name="Gnosis Chain", wallet=WalletAccount.objects.create( @@ -43,6 +49,7 @@ def test_token_distribution_creation(self): td = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.user_profile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -55,19 +62,20 @@ def test_token_distribution_creation(self): deadline=timezone.now() + timezone.timedelta(days=7), # permissions=[self.permission], ) - td.permissions.set([self.permission]) + td.constraints.set([self.permission]) self.assertEqual(TokenDistribution.objects.count(), 1) self.assertEqual(TokenDistribution.objects.first(), td) - self.assertEqual(TokenDistribution.objects.first().permissions.count(), 1) + self.assertEqual(TokenDistribution.objects.first().constraints.count(), 1) self.assertEqual( - TokenDistribution.objects.first().permissions.first(), self.permission + TokenDistribution.objects.first().constraints.first(), self.permission ) def test_token_distribution_expiration(self): td1 = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.user_profile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -84,6 +92,7 @@ def test_token_distribution_expiration(self): td2 = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.user_profile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -122,6 +131,7 @@ def setUp(self) -> None: self.td = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.userprofile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -192,6 +202,7 @@ def setUp(self) -> None: self.td = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.user_profile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -222,11 +233,12 @@ def setUp(self) -> None: type="TIME", ) - self.td.permissions.set([self.permission1, self.permission4]) + self.td.constraints.set([self.permission1, self.permission4]) self.btc_td = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.user_profile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -239,7 +251,7 @@ def setUp(self) -> None: max_number_of_claims=10, notes="Test Notes", ) - self.btc_td.permissions.set([self.permission1, self.permission5]) + self.btc_td.constraints.set([self.permission1, self.permission5]) def test_token_distribution_list(self): response = self.client.get(reverse("token-distribution-list")) @@ -247,13 +259,14 @@ def test_token_distribution_list(self): self.assertEqual(len(response.data), 2) self.assertEqual(response.data[0]["name"], "Test Distribution") self.assertEqual( - response.data[0]["permissions"][0]["name"], "core.BrightIDMeetVerification" + response.data[0]["constraints"][0]["name"], "core.BrightIDMeetVerification" ) def test_token_distribution_not_claimable_max_reached(self): ltd = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.user_profile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -281,6 +294,7 @@ def test_token_distribution_not_claimable_deadline_reached(self): ltd = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.user_profile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -506,6 +520,7 @@ def setUp(self) -> None: self.td = TokenDistribution.objects.create( name="Test Distribution", distributor="Test distributor", + distributor_profile=self.user_profile, distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", @@ -525,7 +540,7 @@ def setUp(self) -> None: # self.permission2 = Constraint.objects.create( # name="core.BrightIDAuraVerification", title="BrightID Aura", type="VER" # ) - self.td.permissions.set([self.permission1]) + self.td.constraints.set([self.permission1]) self.tdc = TokenDistributionClaim.objects.create( user_profile=self.user_profile, @@ -555,7 +570,11 @@ def test_token_distribution_claim_retrieve(self): def test_successful_update(self): claim = TokenDistributionClaim.objects.create( token_distribution=TokenDistribution.objects.create( - token_address="0x123", amount=100, chain=self.chain + distributor_profile=self.user_profile, + token_address="0x123", + amount=100, + chain=self.chain, + deadline=timezone.now() + timezone.timedelta(days=7), ), user_profile=self.user_profile, status=ClaimReceipt.PENDING, @@ -573,7 +592,11 @@ def test_successful_update(self): def test_missing_tx_hash(self): claim = TokenDistributionClaim.objects.create( token_distribution=TokenDistribution.objects.create( - token_address="0x123", amount=100, chain=self.chain + distributor_profile=self.user_profile, + token_address="0x123", + amount=100, + chain=self.chain, + deadline=timezone.now() + timezone.timedelta(days=7), ), user_profile=self.user_profile, status=ClaimReceipt.PENDING, @@ -590,7 +613,11 @@ def test_claim_not_belonging_to_user_profile(self): other_user_profile = UserProfile.objects.get_or_create("other") claim = TokenDistributionClaim.objects.create( token_distribution=TokenDistribution.objects.create( - token_address="0x123", amount=100, chain=self.chain + distributor_profile=self.user_profile, + token_address="0x123", + amount=100, + chain=self.chain, + deadline=timezone.now() + timezone.timedelta(days=7), ), user_profile=other_user_profile, status=ClaimReceipt.PENDING, @@ -605,7 +632,11 @@ def test_claim_not_belonging_to_user_profile(self): def test_already_verified_claim(self): claim = TokenDistributionClaim.objects.create( token_distribution=TokenDistribution.objects.create( - token_address="0x123", amount=100, chain=self.chain + distributor_profile=self.user_profile, + token_address="0x123", + amount=100, + chain=self.chain, + deadline=timezone.now() + timezone.timedelta(days=7), ), user_profile=self.user_profile, status=ClaimReceipt.VERIFIED, diff --git a/tokenTap/views.py b/tokenTap/views.py index bb5ee89c..c4f25613 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -9,6 +9,7 @@ from rest_framework.exceptions import PermissionDenied from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveAPIView from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -19,6 +20,7 @@ from tokenTap.models import TokenDistribution, TokenDistributionClaim from tokenTap.serializers import ( ConstraintSerializer, + CreateTokenDistributionSerializer, DetailResponseSerializer, TokenDistributionClaimResponseSerializer, TokenDistributionClaimSerializer, @@ -58,7 +60,7 @@ def check_token_distribution_is_claimable(self, token_distribution): ) def check_user_permissions(self, token_distribution, user_profile): - for c in token_distribution.permissions.all(): + for c in token_distribution.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)(user_profile) constraint.response = c.response if not constraint.is_observed(token_distribution=token_distribution): @@ -193,7 +195,7 @@ def get(self, request, td_id): response_constraints = [] - for c in td.permissions.all(): + for c in td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)(user_profile) constraint.response = c.response try: @@ -282,3 +284,16 @@ def get(self, request): } ) return Response({"success": True, "data": response}) + + +class CreateTokenDistribution(CreateAPIView): + permission_classes = [IsAuthenticated] + serializer_class = CreateTokenDistributionSerializer + + def post(self, request: Request): + serializer: CreateTokenDistributionSerializer = self.get_serializer( + data=request.data, context={"user_profile": request.user.profile} + ) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response({"success": True, "data": serializer.data})