diff --git a/authentication/models.py b/authentication/models.py index dc5e5f98..d8ee7cdb 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -3,6 +3,7 @@ from authentication.helpers import BRIGHTID_SOULDBOUND_INTERFACE from django.utils import timezone from django.core.validators import RegexValidator +from django.core.cache import cache class ProfileManager(models.Manager): @@ -66,7 +67,7 @@ def is_aura_verified(self): ) return is_verified - + def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -74,6 +75,15 @@ def save(self, *args, **kwargs): self.username = f"User{self.pk}" super().save(*args, **kwargs) + @staticmethod + def user_count(): + cached_user_count = cache.get("user_profile_count") + if cached_user_count: + return cached_user_count + count = UserProfile.objects.count() + cache.set("user_profile_count", count, 300) + return count + class NetworkTypes: EVM = "EVM" diff --git a/authentication/urls.py b/authentication/urls.py index 25e3f072..3ec01b5e 100644 --- a/authentication/urls.py +++ b/authentication/urls.py @@ -5,6 +5,7 @@ urlpatterns = [ path("user/login/", LoginView.as_view(), name="login-user"), + path("user/count/", UserProfileCountView.as_view(), name="user-count"), path( "user/set-username/", SetUsernameView.as_view(), diff --git a/authentication/views.py b/authentication/views.py index 71f2a929..4c668f91 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -22,9 +22,13 @@ ) +class UserProfileCountView(ListAPIView): + def get(self, request, *args, **kwargs): + return Response({"count": UserProfile.user_count()}, status=200) + + class SponsorView(CreateAPIView): def post(self, request, *args, **kwargs): - address = request.data.get("address", None) if not address: return Response({"message": "Invalid request"}, status=403) diff --git a/brightIDfaucet/celery.py b/brightIDfaucet/celery.py index 2502e359..c5f9c812 100644 --- a/brightIDfaucet/celery.py +++ b/brightIDfaucet/celery.py @@ -38,6 +38,14 @@ "task": "faucet.tasks.update_tokentap_claim_for_verified_lightning_claims", "schedule": 3, }, + 'update-tokens-price': { + "task": "faucet.tasks.update_tokens_price", + "schedule": 120, + }, + 'update-donation-receipt-status': { + "task": "faucet.tasks.update_donation_receipt_pending_status", + "schedule": 60, + } } # Load task modules from all registered Django apps. diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index ac640893..92c29801 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -106,6 +106,7 @@ def before_send(event, hint): "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "core.apps.CoreConfig", "faucet.apps.FaucetConfig", "tokenTap.apps.TokentapConfig", "prizetap.apps.PrizetapConfig", @@ -116,8 +117,7 @@ def before_send(event, hint): "encrypted_model_fields", "drf_yasg", "corsheaders", - "django_filters", - "core" + "django_filters" ] MIDDLEWARE = [ diff --git a/core/admin.py b/core/admin.py index fdb05baa..892b41a2 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,4 +1,6 @@ from django.contrib import admin +from .models import TokenPrice + class UserConstraintBaseAdmin(admin.ModelAdmin): fields = [ @@ -12,4 +14,18 @@ class UserConstraintBaseAdmin(admin.ModelAdmin): "pk", "name", "description" - ] \ No newline at end of file + ] + + +class TokenPriceAdmin(admin.ModelAdmin): + list_display = [ + 'symbol', + 'usd_price', + 'price_url', + 'datetime', + 'last_updated' + ] + list_filter = ["symbol"] + + +admin.site.register(TokenPrice, TokenPriceAdmin) diff --git a/core/apps.py b/core/apps.py new file mode 100644 index 00000000..8115ae60 --- /dev/null +++ b/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' diff --git a/core/constraints.py b/core/constraints.py index 6563ada8..a4fbac79 100644 --- a/core/constraints.py +++ b/core/constraints.py @@ -1,17 +1,52 @@ +import copy +from enum import Enum from abc import ABC, abstractmethod from authentication.models import UserProfile +class ConstraintParam(Enum): + CHAIN='chain' + ADDRESS='address' + ID='id' + USERNAME='username' + FROM_DATE='from_date' + TO_DATE='to_date' + + @classmethod + def choices(cls): + return [(key.value, key.name) for key in cls] + class ConstraintVerification(ABC): - def __init__(self, user_profile:UserProfile, response:str = None) -> None: + _param_keys = [] + _param_values = {} + __response_text = "" + + def __init__(self, user_profile:UserProfile) -> None: self.user_profile = user_profile - self.response_text = response @abstractmethod def is_observed(self, *args, **kwargs) -> bool: pass + @classmethod + def param_keys(cls) -> list: + return cls._param_keys + + @classmethod + def set_param_values(cls, values: dict): + valid_keys = [key for key in cls.param_keys()] + for key in values: + if key not in valid_keys: + raise Exception(f"Invalid param key {key}") + cls._param_values = copy.deepcopy(values) + + @property def response(self) -> str: - return self.response_text or f"{self.__class__.__name__} constraint is violated" + return self.__response_text or f"{self.__class__.__name__} constraint is violated" + + @response.setter + def response(self, text: str): + self.__response_text = text + class BrightIDMeetVerification(ConstraintVerification): def is_observed(self, *args, **kwargs): @@ -21,4 +56,19 @@ def is_observed(self, *args, **kwargs): class BrightIDAuraVerification(ConstraintVerification): def is_observed(self, *args, **kwargs): return self.user_profile.is_aura_verified + +class HasNFTVerification(ConstraintVerification): + _param_keys = [ + ConstraintParam.CHAIN, + ConstraintParam.ADDRESS, + ConstraintParam.ID + ] + + def __init__(self, user_profile: UserProfile, response: str = None) -> None: + super().__init__(user_profile, response) + + def is_observed(self, *args, **kwargs): + chain_id = self._param_values[ConstraintParam.CHAIN] + collection = self._param_values[ConstraintParam.ADDRESS] + nft_id = self._param_values[ConstraintParam.ID] \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 00000000..6b3034c7 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.4 on 2023-08-27 22:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='TokenPrice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('usd_price', models.CharField(max_length=255)), + ('datetime', models.DateTimeField(auto_now_add=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('price_url', models.URLField(blank=True, max_length=255)), + ('symbol', models.CharField(db_index=True, max_length=255, unique=True)), + ], + ), + ] diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/core/models.py b/core/models.py index 268e1e76..357ebd95 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ from django.db import models -from .constraints import * from django.utils.translation import gettext_lazy as _ +from .constraints import * + class UserConstraint(models.Model): class Meta: @@ -15,8 +16,7 @@ class Type(models.TextChoices): BrightIDAuraVerification ] - - name = models.CharField(max_length=255, unique=True, + name = models.CharField(max_length=255, unique=True, choices=[(c.__name__, c.__name__) for c in constraints]) title = models.CharField(max_length=255) type = models.CharField( @@ -32,5 +32,13 @@ def __str__(self) -> str: @classmethod def create_name_field(cls, constraints): - return models.CharField(max_length=255, unique=True, - choices=[(c.__name__, c.__name__) for c in constraints]) \ No newline at end of file + return models.CharField(max_length=255, unique=True, + choices=[(c.__name__, c.__name__) for c in constraints]) + + +class TokenPrice(models.Model): + usd_price = models.CharField(max_length=255, null=False) + datetime = models.DateTimeField(auto_now_add=True) + last_updated = models.DateTimeField(auto_now=True, null=True, blank=True) + price_url = models.URLField(max_length=255, blank=True, null=False) + symbol = models.CharField(max_length=255, db_index=True, unique=True, null=False, blank=False) diff --git a/core/serializers.py b/core/serializers.py index e24d2ffa..904f9782 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from .constraints import * from .models import UserConstraint class UserConstraintBaseSerializer(serializers.Serializer): @@ -10,6 +11,7 @@ class UserConstraintBaseSerializer(serializers.Serializer): ) description = serializers.CharField() response = serializers.CharField() + params = serializers.SerializerMethodField() class Meta: fields = [ @@ -18,7 +20,8 @@ class Meta: "title", "type", "description", - "response" + "response", + "params" ] read_only_fields = [ "pk", @@ -26,5 +29,10 @@ class Meta: "title", "type", "description", - "response" - ] \ No newline at end of file + "response", + "params" + ] + + def get_params(self, constraint: UserConstraint): + c_class: ConstraintVerification = eval(constraint.name) + return [p.value for p in c_class.param_keys()] \ No newline at end of file diff --git a/core/tests.py b/core/tests.py index 0d17ca21..6dd60423 100644 --- a/core/tests.py +++ b/core/tests.py @@ -24,11 +24,11 @@ def test_meet_constraint(self, is_meet_verified_mock:PropertyMock): is_meet_verified_mock.return_value = False constraint = BrightIDMeetVerification(self.user_profile) self.assertEqual(constraint.is_observed(), False) - self.assertEqual(constraint.response(), "BrightIDMeetVerification constraint is violated") + self.assertEqual(constraint.response, "BrightIDMeetVerification constraint is violated") @patch('authentication.models.UserProfile.is_aura_verified', new_callable=PropertyMock) def test_aura_constraint(self, is_aura_verified_mock:PropertyMock): is_aura_verified_mock.return_value = False constraint = BrightIDAuraVerification(self.user_profile) self.assertEqual(constraint.is_observed(), False) - self.assertEqual(constraint.response(), "BrightIDAuraVerification constraint is violated") \ No newline at end of file + self.assertEqual(constraint.response, "BrightIDAuraVerification constraint is violated") \ No newline at end of file diff --git a/faucet/admin.py b/faucet/admin.py index a5b45f21..37430a21 100644 --- a/faucet/admin.py +++ b/faucet/admin.py @@ -72,7 +72,19 @@ class TransactionBatchAdmin(admin.ModelAdmin): class LightningConfigAdmin(admin.ModelAdmin): readonly_fields = ["claimed_amount", "current_round"] list_display = ["pk", "period", "period_max_cap", "claimed_amount", "current_round"] - pass + + +class DonationReceiptAdmin(admin.ModelAdmin): + list_display = [ + 'tx_hash', + 'user_profile', + 'chain', + 'value', + 'total_price', + 'datetime' + ] + search_fields = ['tx_hash'] + list_filter = ['chain', 'user_profile'] admin.site.register(WalletAccount, WalletAccountAdmin) @@ -82,3 +94,4 @@ class LightningConfigAdmin(admin.ModelAdmin): admin.site.register(GlobalSettings, GlobalSettingsAdmin) admin.site.register(TransactionBatch, TransactionBatchAdmin) admin.site.register(LightningConfig, LightningConfigAdmin) +admin.site.register(DonationReceipt, DonationReceiptAdmin) diff --git a/faucet/faucet_manager/fund_manager.py b/faucet/faucet_manager/fund_manager.py index ebe6a692..75d97533 100644 --- a/faucet/faucet_manager/fund_manager.py +++ b/faucet/faucet_manager/fund_manager.py @@ -69,12 +69,16 @@ def is_gas_price_too_high(self): def account(self) -> LocalAccount: return self.w3.eth.account.privateKeyToAccount(self.chain.wallet.main_key) - def get_checksum_address(self): - return Web3.toChecksumAddress(self.chain.fund_manager_address.lower()) + @staticmethod + def to_checksum_address(address: str): + return Web3.toChecksumAddress(address.lower()) + + def get_fund_manager_checksum_address(self): + return self.to_checksum_address(self.chain.fund_manager_address) @property def contract(self): - return self.w3.eth.contract(address=self.get_checksum_address(), abi=self.abi) + return self.w3.eth.contract(address=self.get_fund_manager_checksum_address(), abi=self.abi) def transfer(self, bright_user: BrightUser, amount: int): tx = self.single_eth_transfer_signed_tx(amount, bright_user.address) @@ -121,13 +125,14 @@ def prepare_tx_for_broadcast(self, tx_function): return signed_tx def is_tx_verified(self, tx_hash): - try: - receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) - if receipt["status"] == 1: - return True - return False - except TimeExhausted: - raise + receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) + if receipt["status"] == 1: + return True + return False + + def get_tx(self, tx_hash): + tx = self.w3.eth.get_transaction(tx_hash) + return tx class SolanaFundManager: @@ -218,7 +223,7 @@ def multi_transfer(self, data): instructions.withdraw( {"amount": item['amount']}, { - "lock_account": self.lock_account_address, + "lock_account": self.lock_account_address, "operator": self.operator, "recipient": Pubkey.from_string(item["to"]) }, diff --git a/faucet/migrations/0053_tokenprice.py b/faucet/migrations/0053_tokenprice.py new file mode 100644 index 00000000..6198d71b --- /dev/null +++ b/faucet/migrations/0053_tokenprice.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.4 on 2023-08-08 08:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0052_alter_claimreceipt__status_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='TokenPrice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('usd_price', models.CharField(max_length=255)), + ('datetime', models.DateTimeField(auto_now_add=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('price_url', models.URLField(blank=True, max_length=255)), + ('symbol', models.CharField(max_length=255)), + ], + ), + ] diff --git a/faucet/migrations/0054_donationreceipt_delete_tokenprice.py b/faucet/migrations/0054_donationreceipt_delete_tokenprice.py new file mode 100644 index 00000000..c631cb12 --- /dev/null +++ b/faucet/migrations/0054_donationreceipt_delete_tokenprice.py @@ -0,0 +1,33 @@ +# Generated by Django 4.0.4 on 2023-08-27 22:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0017_alter_userprofile_username'), + ('faucet', '0053_tokenprice'), + ] + + operations = [ + migrations.CreateModel( + name='DonationReceipt', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tx_hash', models.CharField(max_length=255)), + ('value', models.CharField(max_length=255)), + ('total_price', models.CharField(max_length=255)), + ('datetime', models.DateTimeField(auto_now_add=True)), + ('chain', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='donation', to='faucet.chain')), + ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='donations', to='authentication.userprofile')), + ], + options={ + 'unique_together': {('chain', 'tx_hash')}, + }, + ), + migrations.DeleteModel( + name='TokenPrice', + ), + ] diff --git a/faucet/migrations/0055_donationreceipt_status_and_more.py b/faucet/migrations/0055_donationreceipt_status_and_more.py new file mode 100644 index 00000000..e05b3afa --- /dev/null +++ b/faucet/migrations/0055_donationreceipt_status_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.0.4 on 2023-09-01 16:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0054_donationreceipt_delete_tokenprice'), + ] + + operations = [ + migrations.AddField( + model_name='donationreceipt', + name='status', + field=models.CharField(choices=[('Pending', 'Pending'), ('Verified', 'Verified'), ('Rejected', 'Rejected'), ('Processed', 'Processed'), ('Processed_Rejected', 'Processed_Rejected')], default='Processed', max_length=30), + ), + migrations.AlterField( + model_name='donationreceipt', + name='total_price', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='donationreceipt', + name='value', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='transactionbatch', + name='_status', + field=models.CharField(choices=[('Pending', 'Pending'), ('Verified', 'Verified'), ('Rejected', 'Rejected'), ('Processed', 'Processed'), ('Processed_Rejected', 'Processed_Rejected')], db_index=True, default='Pending', max_length=30), + ), + migrations.AlterField( + model_name='transactionbatch', + name='tx_hash', + field=models.CharField(blank=True, db_index=True, max_length=255, null=True), + ), + ] diff --git a/faucet/models.py b/faucet/models.py index bf9be9c4..1bf3347a 100644 --- a/faucet/models.py +++ b/faucet/models.py @@ -1,3 +1,4 @@ +from decimal import Decimal from datetime import datetime, timedelta import logging from django.db import models @@ -207,6 +208,17 @@ def tx_hash(self): return self.batch.tx_hash return None + @staticmethod + def claims_count(): + cached_count = cache.get("gastap_claims_count") + if cached_count: + return cached_count + count = ClaimReceipt.objects.filter( + _status__in=[ClaimReceipt.VERIFIED, BrightUser.VERIFIED] + ).count() + cache.set("gastap_claims_count", count, 600) + return count + class Chain(models.Model): chain_name = models.CharField(max_length=255) @@ -338,7 +350,10 @@ def get_wallet_balance(self): ) return lnpay_client.get_balance() raise Exception("Invalid chain type") - except: + except Exception as e: + logging.exception( + f"Error getting wallet balance for {self.chain_name} error is {e}" + ) return 0 @property @@ -460,12 +475,12 @@ class GlobalSettings(models.Model): class TransactionBatch(models.Model): - chain = models.ForeignKey(Chain, related_name="batches", on_delete=models.PROTECT) + chain = models.ForeignKey(Chain, related_name="batches", on_delete=models.PROTECT, db_index=True) datetime = models.DateTimeField(auto_now_add=True) - tx_hash = models.CharField(max_length=255, blank=True, null=True) + tx_hash = models.CharField(max_length=255, blank=True, null=True, db_index=True) _status = models.CharField( - max_length=30, choices=ClaimReceipt.states, default=ClaimReceipt.PENDING + max_length=30, choices=ClaimReceipt.states, default=ClaimReceipt.PENDING, db_index=True ) updating = models.BooleanField(default=False) @@ -514,3 +529,30 @@ class LightningConfig(models.Model): def save(self, *args, **kwargs): self.pk = 1 super().save(*args, **kwargs) + + +class DonationReceipt(models.Model): + user_profile = models.ForeignKey( + UserProfile, + related_name="donations", + on_delete=models.PROTECT, + null=False, + blank=False + ) + tx_hash = models.CharField(max_length=255, blank=False, null=False) + chain = models.ForeignKey( + Chain, + related_name="donation", + on_delete=models.PROTECT, + null=False, + blank=False, + ) + value = models.CharField(max_length=255, null=True, blank=True) + total_price = models.CharField(max_length=255, null=True, blank=True) + datetime = models.DateTimeField(auto_now_add=True) + status = models.CharField( + max_length=30, choices=ClaimReceipt.states, default=ClaimReceipt.PROCESSED_FOR_TOKENTAP + ) + + class Meta: + unique_together = ('chain', 'tx_hash') diff --git a/faucet/serializers.py b/faucet/serializers.py index a2b7bfcf..17ee57d7 100644 --- a/faucet/serializers.py +++ b/faucet/serializers.py @@ -1,9 +1,17 @@ +import decimal + +from django.db.models import Func, F from rest_framework import serializers +from rest_framework import status +import web3.exceptions + from authentication.models import UserProfile from faucet.faucet_manager.claim_manager import LimitedChainClaimManager from faucet.faucet_manager.credit_strategy import CreditStrategyFactory -from faucet.models import BrightUser, Chain, ClaimReceipt, GlobalSettings +from faucet.models import BrightUser, Chain, ClaimReceipt, GlobalSettings, DonationReceipt +from faucet.faucet_manager.fund_manager import EVMFundManager +from core.models import TokenPrice class UserSerializer(serializers.ModelSerializer): @@ -25,8 +33,8 @@ 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) ) def create(self, validated_data): @@ -167,3 +175,43 @@ class Meta: "status", "last_updated", ] + + +class DonationReceiptSerializer(serializers.ModelSerializer): + chain_name = serializers.CharField(max_length=20, write_only=True) + chain = SmallChainSerializer(read_only=True) + + def validate(self, attrs): + chain = self._validate_chain(attrs.pop('chain_name')) + attrs['user_profile'] = self.context.get('user') + attrs['chain'] = chain + return attrs + + def _validate_chain(self, chain_name: str): + try: + chain: Chain = Chain.objects.get(chain_name=chain_name, chain_type='EVM') + except Chain.DoesNotExist: + raise serializers.ValidationError({'chain': 'chain is not EVM or does not exist.'}) + return chain + + class Meta: + model = DonationReceipt + depth = 1 + fields = [ + "tx_hash", + "chain", + "datetime", + "total_price", + "value", + "chain_name", + "status", + "user_profile" + ] + read_only_fields = [ + 'value', + 'datetime', + 'total_price', + 'chain', + 'status', + "user_profile" + ] diff --git a/faucet/tasks.py b/faucet/tasks.py index b7c49257..90c452a8 100644 --- a/faucet/tasks.py +++ b/faucet/tasks.py @@ -1,10 +1,13 @@ +import decimal import time import logging from contextlib import contextmanager +import web3.exceptions +import requests from celery import shared_task from django.core.cache import cache from django.db import transaction -from django.db.models import Q +from django.db.models import Q, F, Func from django.utils import timezone from sentry_sdk import capture_exception from authentication.models import NetworkTypes, Wallet @@ -16,7 +19,8 @@ LightningFundManager, FundMangerException, ) -from .models import Chain, ClaimReceipt, TransactionBatch +from core.models import TokenPrice +from .models import Chain, ClaimReceipt, TransactionBatch, DonationReceipt @contextmanager @@ -96,8 +100,8 @@ def process_batch(self, batch_pk): elif batch.chain.chain_type == NetworkTypes.LIGHTNING: manager = LightningFundManager(batch.chain) elif ( - batch.chain.chain_type == NetworkTypes.EVM - or batch.chain.chain_type == NetworkTypes.NONEVMXDC + batch.chain.chain_type == NetworkTypes.EVM + or batch.chain.chain_type == NetworkTypes.NONEVMXDC ): manager = EVMFundManager(batch.chain) else: @@ -150,8 +154,8 @@ def update_pending_batch_with_tx_hash(self, batch_pk): elif batch.chain.chain_type == NetworkTypes.LIGHTNING: manager = LightningFundManager(batch.chain) elif ( - batch.chain.chain_type == NetworkTypes.EVM - or batch.chain.chain_type == NetworkTypes.NONEVMXDC + batch.chain.chain_type == NetworkTypes.EVM + or batch.chain.chain_type == NetworkTypes.NONEVMXDC ): manager = EVMFundManager(batch.chain) else: @@ -181,7 +185,7 @@ def reject_expired_pending_claims(): batch=None, _status=ClaimReceipt.PENDING, datetime__lte=timezone.now() - - timezone.timedelta(minutes=ClaimReceipt.MAX_PENDING_DURATION), + - timezone.timedelta(minutes=ClaimReceipt.MAX_PENDING_DURATION), ).update(_status=ClaimReceipt.REJECTED) @@ -338,3 +342,88 @@ def update_tokentap_claim_for_verified_lightning_claims(): process_rejected_lighning_claim.delay( _claim.pk, ) + + +@shared_task +def update_tokens_price(): + """ + update token.usd_price for all TokenPrice records in DB + """ + + # TODO: we can make this function performance better by using aiohttp and asyncio or Threads + tokens = TokenPrice.objects.all() + res_gen = map(lambda token: (token, requests.get(token.price_url, timeout=5)), tokens) + + def parse_request(token: TokenPrice, request_res: requests.Response): + try: + request_res.raise_for_status() + json_data = request_res.json() + token.usd_price = json_data['data']['rates']['USD'] + # TODO: save all change when this function ended for all url done for better performance + token.save() + except requests.HTTPError as e: + logging.exception( + f'requests for url: {request_res.url} can not fetched with status_code: {request_res.status_code}. \ + {str(e)}') + + except KeyError as e: + logging.exception( + f'requests for url: {request_res.url} data do not have property keys for loading data. {str(e)}') + + except Exception as e: + logging.exception(f'requests for url: {request_res.url} got error {type(e).__name__}. {str(e)}') + + [parse_request(*res) for res in res_gen] + + +@shared_task(bind=True) +def process_donation_receipt(self, donation_receipt_pk): + lock_name = f'{self.name}-LOCK-{donation_receipt_pk}' + logging.info(f'lock name is: {lock_name}') + with memcache_lock(lock_name, self.app.oid) as acquired: + donation_receipt = DonationReceipt.objects.get(pk=donation_receipt_pk) + if not acquired: + logging.debug("Could not acquire update lock") + return + evm_fund_manager = EVMFundManager(donation_receipt.chain) + try: + if evm_fund_manager.is_tx_verified(donation_receipt.tx_hash) is False: + donation_receipt.delete() + return + user = donation_receipt.user_profile + tx = evm_fund_manager.get_tx(donation_receipt.tx_hash) + if tx.get('from').lower() not in user.wallets.annotate( + lower_address=Func(F('address'), function='LOWER')).values_list('lower_address', flat=True): + donation_receipt.delete() + return + if evm_fund_manager.to_checksum_address( + tx.get('to')) != evm_fund_manager.get_fund_manager_checksum_address(): + donation_receipt.delete() + return + donation_receipt.value = tx.get('value') + if donation_receipt.chain.is_testnet is False: + try: + token_price = TokenPrice.objects.get(symbol=donation_receipt.chain.symbol) + donation_receipt.total_price = str( + decimal.Decimal(tx.get('value')) * decimal.Decimal(token_price.usd_price)) + except TokenPrice.DoesNotExist: + logging.error(f'TokenPrice for Chain: {donation_receipt.chain.chain_name} did not defined') + donation_receipt.status = ClaimReceipt.PROCESSED_FOR_TOKENTAP_REJECT + return + else: + donation_receipt.total_price = str(0) + donation_receipt.status = ClaimReceipt.VERIFIED + donation_receipt.save() + except (web3.exceptions.TransactionNotFound, web3.exceptions.TimeExhausted): + donation_receipt.delete() + return + + +@shared_task +def update_donation_receipt_pending_status(): + """ + update status of pending donation receipt + """ + pending_donation_receipts = DonationReceipt.objects.filter(status=ClaimReceipt.PROCESSED_FOR_TOKENTAP) + for pending_donation_receipt in pending_donation_receipts: + process_donation_receipt.delay(pending_donation_receipt.pk) diff --git a/faucet/urls.py b/faucet/urls.py index c0fa6784..36248080 100644 --- a/faucet/urls.py +++ b/faucet/urls.py @@ -2,6 +2,7 @@ from faucet.views import ( ChainListView, + ClaimCountView, ClaimMaxView, GlobalSettingsView, LastClaimView, @@ -11,6 +12,7 @@ error500, ChainBalanceView, SmallChainListView, + DonationReceiptView, ) from drf_yasg.views import get_schema_view @@ -37,6 +39,7 @@ ), path("user/last-claim/", LastClaimView.as_view(), name="last-claim"), path("user/claims/", ListClaims.as_view(), name="claims"), + path("claims/count/", ClaimCountView.as_view(), name="claims-count"), path( "chain/list/", ChainListView.as_view(), name="chain-list" ), # can have auth token for more user specific info @@ -60,4 +63,5 @@ ChainBalanceView.as_view(), name="chain-balance", ), + path("user/donation", DonationReceiptView.as_view(), name="donation-receipt"), ] diff --git a/faucet/views.py b/faucet/views.py index d414b9aa..8827336a 100644 --- a/faucet/views.py +++ b/faucet/views.py @@ -4,7 +4,12 @@ import os import rest_framework.exceptions from django.http import Http404 -from rest_framework.generics import CreateAPIView, RetrieveAPIView, ListAPIView +from rest_framework.generics import ( + CreateAPIView, + RetrieveAPIView, + ListAPIView, + ListCreateAPIView, +) from rest_framework.response import Response from rest_framework.views import APIView @@ -17,13 +22,14 @@ LimitedChainClaimManager, ) from faucet.faucet_manager.claim_manager import WeeklyCreditStrategy -from faucet.models import Chain, ClaimReceipt, GlobalSettings +from faucet.models import Chain, ClaimReceipt, GlobalSettings, DonationReceipt from faucet.serializers import ( ChainBalanceSerializer, GlobalSettingsSerializer, ReceiptSerializer, ChainSerializer, SmallChainSerializer, + DonationReceiptSerializer, ) # import BASE_DIR from django settings @@ -34,6 +40,11 @@ class CustomException(Exception): pass +class ClaimCountView(ListAPIView): + def get(self, request, *args, **kwargs): + return Response({"count": ClaimReceipt.claims_count()}, status=200) + + class LastClaimView(RetrieveAPIView): serializer_class = ReceiptSerializer @@ -207,6 +218,22 @@ def get_object(self): return Chain.objects.get(pk=chain_pk) +class DonationReceiptView(ListCreateAPIView): + permission_classes = [IsAuthenticated] + serializer_class = DonationReceiptSerializer + + def get_serializer_context(self): + context = super().get_serializer_context() + context.update({"user": self.get_user()}) + return context + + def get_queryset(self): + return DonationReceipt.objects.filter(user_profile=self.get_user()) + + def get_user(self) -> UserProfile: + return self.request.user.profile + + def artwork_video(request): video_file = os.path.join(settings.BASE_DIR, f"faucet/artwork.mp4") return FileResponse(open(video_file, "rb"), content_type="video/mp4") diff --git a/prizetap/admin.py b/prizetap/admin.py index 8bd9ec30..ec2ead7b 100644 --- a/prizetap/admin.py +++ b/prizetap/admin.py @@ -10,18 +10,12 @@ class RaffleŁEntryAdmin(admin.ModelAdmin): list_display = [ "pk", "raffle", - "get_wallet", - "signature", - "nonce" + "get_wallet" ] @admin.display(ordering='user_profile__wallets', description='Wallet') def get_wallet(self, obj): return obj.user_profile.wallets.get(wallet_type=NetworkTypes.EVM).address - - @admin.display(ordering='pk') - def nonce(self, obj): - return obj.pk admin.site.register(Raffle, RaffleAdmin) diff --git a/prizetap/migrations/0015_raffle_constraint_params.py b/prizetap/migrations/0015_raffle_constraint_params.py new file mode 100644 index 00000000..499e1344 --- /dev/null +++ b/prizetap/migrations/0015_raffle_constraint_params.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-08-09 11:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0014_constraint_description_constraint_title'), + ] + + operations = [ + migrations.AddField( + model_name='raffle', + name='constraint_params', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/prizetap/migrations/0016_remove_raffle_signer.py b/prizetap/migrations/0016_remove_raffle_signer.py new file mode 100644 index 00000000..ef4ed169 --- /dev/null +++ b/prizetap/migrations/0016_remove_raffle_signer.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.4 on 2023-08-15 06:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0015_raffle_constraint_params'), + ] + + operations = [ + migrations.RemoveField( + model_name='raffle', + name='signer', + ), + ] diff --git a/prizetap/migrations/0017_remove_raffleentry_signature.py b/prizetap/migrations/0017_remove_raffleentry_signature.py new file mode 100644 index 00000000..bbe2ff1c --- /dev/null +++ b/prizetap/migrations/0017_remove_raffleentry_signature.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.4 on 2023-08-15 06:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0016_remove_raffle_signer'), + ] + + operations = [ + migrations.RemoveField( + model_name='raffleentry', + name='signature', + ), + ] diff --git a/prizetap/migrations/0018_raffle_max_multiplier_raffle_start_at.py b/prizetap/migrations/0018_raffle_max_multiplier_raffle_start_at.py new file mode 100644 index 00000000..01220b64 --- /dev/null +++ b/prizetap/migrations/0018_raffle_max_multiplier_raffle_start_at.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.4 on 2023-08-15 09:36 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0017_remove_raffleentry_signature'), + ] + + operations = [ + migrations.AddField( + model_name='raffle', + name='max_multiplier', + field=models.IntegerField(default=1), + ), + migrations.AddField( + model_name='raffle', + name='start_at', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/prizetap/migrations/0019_alter_raffle_chain.py b/prizetap/migrations/0019_alter_raffle_chain.py new file mode 100644 index 00000000..6607bb2a --- /dev/null +++ b/prizetap/migrations/0019_alter_raffle_chain.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.4 on 2023-08-27 08:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0053_tokenprice'), + ('prizetap', '0018_raffle_max_multiplier_raffle_start_at'), + ] + + operations = [ + migrations.AlterField( + model_name='raffle', + name='chain', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='raffles', to='faucet.chain'), + ), + ] diff --git a/prizetap/models.py b/prizetap/models.py index f90e2daa..770bc0c4 100644 --- a/prizetap/models.py +++ b/prizetap/models.py @@ -2,11 +2,6 @@ from faucet.models import Chain from django.utils import timezone from authentication.models import NetworkTypes, UserProfile -from faucet.models import WalletAccount -from .utils import ( - raffle_hash_message, - sign_hashed_message -) from core.models import UserConstraint # Create your models here. @@ -25,7 +20,6 @@ class Meta: description = models.TextField(null=True, blank=True) contract = models.CharField(max_length=256) raffleId = models.BigIntegerField() - signer = models.ForeignKey(WalletAccount, on_delete=models.DO_NOTHING) creator = models.CharField(max_length=256, null=True, blank=True) creator_url = models.URLField(max_length=255, null=True, blank=True) discord_url = models.URLField(max_length=255, null=True, blank=True) @@ -44,17 +38,24 @@ class Meta: token_uri = models.TextField(null=True, blank=True) chain = models.ForeignKey( - Chain, on_delete=models.CASCADE, related_name="raffles", null=True, blank=True + Chain, on_delete=models.CASCADE, related_name="raffles" ) constraints = models.ManyToManyField(Constraint, blank=True, related_name="raffles") + constraint_params = models.TextField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=True) + start_at = models.DateTimeField(default=timezone.now) deadline = models.DateTimeField(null=True, blank=True) max_number_of_entries = models.IntegerField(null=True, blank=True) + max_multiplier = models.IntegerField(default=1) is_active = models.BooleanField(default=True) + @property + def is_started(self): + return timezone.now() >= self.start_at + @property def is_expired(self): if self.deadline is None: @@ -69,12 +70,16 @@ def is_maxed_out(self): @property def is_claimable(self): - return not self.is_expired and not self.is_maxed_out and self.is_active + return self.is_started and not self.is_expired and \ + not self.is_maxed_out and self.is_active @property def number_of_entries(self): - return self.entries.filter(tx_hash__isnull=False).aggregate( - TOTAL = models.Sum('multiplier'))['TOTAL'] or 0 + return self.entries.count() + + @property + def number_of_onchain_entries(self): + return self.entries.filter(tx_hash__isnull=False).count() @property def winner(self): @@ -91,20 +96,6 @@ def winner_entry(self): def __str__(self): return f"{self.name}" - - def generate_signature(self, user: str, nonce: int = None, multiplier: int = None): - assert self.raffleId and self.signer - - hashed_message = raffle_hash_message( - user=user, - raffleId=self.raffleId, - nonce=nonce, - multiplier=multiplier - ) - - return sign_hashed_message( - hashed_message, self.signer.private_key - ) class RaffleEntry(models.Model): @@ -119,7 +110,6 @@ class Meta: created_at = models.DateTimeField(auto_now_add=True, editable=True) - signature = models.CharField(max_length=1024, blank=True, null=True) multiplier = models.IntegerField(default=1) is_winner = models.BooleanField(blank=True, default=False) tx_hash = models.CharField(max_length=255, blank=True, null=True) @@ -131,10 +121,6 @@ def __str__(self): @property def user(self): return self.user_profile.wallets.get(wallet_type=NetworkTypes.EVM).address - - @property - def nonce(self): - return self.pk def save(self, *args, **kwargs): if self.is_winner: diff --git a/prizetap/serializers.py b/prizetap/serializers.py index ec00e466..d53e58a7 100644 --- a/prizetap/serializers.py +++ b/prizetap/serializers.py @@ -8,16 +8,68 @@ class ConstraintSerializer(UserConstraintBaseSerializer, serializers.ModelSerial class Meta(UserConstraintBaseSerializer.Meta): ref_name = "RaffleConstraint" model = Constraint - + +class SimpleRaffleSerializer(serializers.ModelSerializer): + class Meta: + model = Raffle + fields = [ + "pk", + "name", + "contract", + "raffleId", + ] + read_only_fields = [ + "pk", + "name", + "contract", + "raffleId", + ] + class RaffleEntrySerializer(serializers.ModelSerializer): + raffle = SimpleRaffleSerializer() + user_profile = SimpleProfilerSerializer() + chain = serializers.SerializerMethodField() + wallet = serializers.SerializerMethodField() + class Meta: + model = RaffleEntry + fields = [ + "pk", + "chain", + "raffle", + "user_profile", + "wallet", + "created_at", + "multiplier", + "tx_hash", + "claiming_prize_tx" + ] + read_only_fields = [ + "pk", + "chain", + "raffle", + "user_profile", + "wallet", + "created_at", + "multiplier", + ] + + def get_chain(self, entry: RaffleEntry): + return entry.raffle.chain.chain_id + + def get_wallet(self, entry: RaffleEntry): + return entry.user_profile.wallets.get( + wallet_type=entry.raffle.chain.chain_type).address + +class WinnerEntrySerializer(serializers.ModelSerializer): user_profile = SimpleProfilerSerializer() + wallet = serializers.SerializerMethodField() class Meta: model = RaffleEntry fields = [ "pk", "user_profile", + "wallet", "created_at", - "signature", "multiplier", "tx_hash", "claiming_prize_tx" @@ -25,19 +77,18 @@ class Meta: read_only_fields = [ "pk", "user_profile", + "wallet", "created_at", - "signature", "multiplier", ] - def to_representation(self, instance: RaffleEntry): - representation = super().to_representation(instance) - representation["nonce"] = instance.nonce - return representation + def get_wallet(self, entry: RaffleEntry): + return entry.user_profile.wallets.get( + wallet_type=entry.raffle.chain.chain_type).address class RaffleSerializer(serializers.ModelSerializer): chain = SmallChainSerializer() - winner_entry = RaffleEntrySerializer() + winner_entry = WinnerEntrySerializer() user_entry = serializers.SerializerMethodField() constraints = ConstraintSerializer(many=True, read_only=True) @@ -64,7 +115,9 @@ class Meta: "contract", "raffleId", "constraints", + "constraint_params", "created_at", + "start_at", "deadline", "max_number_of_entries", "is_active", @@ -73,6 +126,8 @@ class Meta: "is_claimable", "user_entry", "number_of_entries", + "number_of_onchain_entries", + "max_multiplier" ] def get_user_entry(self, raffle: Raffle): diff --git a/prizetap/tests.py b/prizetap/tests.py index 72748c6f..c74a01dc 100644 --- a/prizetap/tests.py +++ b/prizetap/tests.py @@ -63,7 +63,6 @@ def setUp(self): description="Test Raffle Description", contract=erc20_contract_address, raffleId=1, - signer=self.wallet, prize_amount=1e14, prize_asset="0x0000000000000000000000000000000000000000", prize_name="Test raffle", @@ -120,6 +119,22 @@ def test_raffle_maxed_out(self): entry.tx_hash = "0x0" entry.save() + self.assertFalse(self.raffle.is_maxed_out) + self.assertTrue(self.raffle.is_claimable) + + RaffleEntry.objects.create( + raffle=self.raffle, + user_profile=UserProfile.objects.create( + user=User.objects.create_user( + username="test_2", + password="1234" + ), + initial_context_id="test_2", + username="test_2", + ), + multiplier=1 + ) + self.assertTrue(self.raffle.is_maxed_out) self.assertFalse(self.raffle.is_claimable) @@ -189,15 +204,7 @@ def test_raffle_enrollment(self): entry: RaffleEntry = self.raffle.entries.first() self.assertEqual(entry.user_profile, self.user_profile) self.assertEqual(entry.is_winner, False) - self.assertEqual(self.raffle.number_of_entries, 0) - self.assertEqual(response.data['signature']['signature'], entry.signature) - self.assertEqual(response.data['signature']['signature'], - self.raffle.generate_signature( - self.user_profile.wallets.get(wallet_type=NetworkTypes.EVM).address, - entry.pk, - 1 - ) - ) + self.assertEqual(self.raffle.number_of_entries, 1) @patch('prizetap.models.Raffle.is_claimable', new_callable=PropertyMock) @patch( @@ -258,7 +265,6 @@ def test_set_not_owned_raffle_enrollment_tx_failure(self): self.assertEqual(response.status_code, 403) entry.refresh_from_db() self.assertEqual(entry.tx_hash, None) - self.assertEqual(self.raffle.number_of_entries, 0) @patch( "authentication.helpers.BrightIDSoulboundAPIInterface.get_verification_status", @@ -278,66 +284,6 @@ def test_duplicate_set_raffle_enrollment_tx_failure(self): ) self.assertEqual(response.status_code, 403) - @patch( - "authentication.helpers.BrightIDSoulboundAPIInterface.get_verification_status", - lambda a, b, c : (True, None) - ) - def test_claim_prize(self): - RaffleEntry.objects.create( - raffle=self.raffle, - user_profile=self.user_profile, - is_winner=True - ) - self.raffle.deadline = timezone.now() - self.raffle.save() - self.client.force_authenticate(user=self.user_profile.user) - response_1 = self.client.post( - reverse("claim-prize", kwargs={"pk": self.raffle.pk}) - ) - self.assertEqual(response_1.status_code, 200) - response_2 = self.client.post( - reverse("claim-prize", kwargs={"pk": self.raffle.pk}) - ) - self.assertEqual(response_2.status_code, 200) - signature = self.raffle.generate_signature( - self.user_profile.wallets.get(wallet_type=NetworkTypes.EVM).address - ) - self.assertEqual(response_1.data['signature'], signature) - self.assertEqual(response_2.data['signature'], signature) - - @patch( - "authentication.helpers.BrightIDSoulboundAPIInterface.get_verification_status", - lambda a, b, c : (True, None) - ) - def test_claim_prize_fail_when_not_winner(self): - RaffleEntry.objects.create( - raffle=self.raffle, - user_profile=self.user_profile - ) - self.raffle.deadline = timezone.now() - self.raffle.save() - self.client.force_authenticate(user=self.user_profile.user) - response = self.client.post( - reverse("claim-prize", kwargs={"pk": self.raffle.pk}) - ) - self.assertEqual(response.status_code, 403) - - @patch( - "authentication.helpers.BrightIDSoulboundAPIInterface.get_verification_status", - lambda a, b, c : (True, None) - ) - def test_claim_prize_fail_when_not_expired(self): - RaffleEntry.objects.create( - raffle=self.raffle, - user_profile=self.user_profile, - is_winner=True - ) - self.client.force_authenticate(user=self.user_profile.user) - response = self.client.post( - reverse("claim-prize", kwargs={"pk": self.raffle.pk}) - ) - self.assertEqual(response.status_code, 403) - @patch( "authentication.helpers.BrightIDSoulboundAPIInterface.get_verification_status", lambda a, b, c : (True, None) diff --git a/prizetap/urls.py b/prizetap/urls.py index efa761a6..e648cb6b 100644 --- a/prizetap/urls.py +++ b/prizetap/urls.py @@ -13,9 +13,9 @@ name="raflle-enrollment", ), path( - "claim-prize//", - ClaimPrizeView.as_view(), - name="claim-prize", + "raffle-enrollment/detail//", + GetRaffleEntryView.as_view(), + name="raflle-enrollment-detail", ), path( "set-enrollment-tx//", diff --git a/prizetap/utils.py b/prizetap/utils.py deleted file mode 100644 index 4676678b..00000000 --- a/prizetap/utils.py +++ /dev/null @@ -1,36 +0,0 @@ -import random -from web3 import Web3, Account -from eth_account.messages import encode_defunct - -def create_uint32_random_nonce(): - # uint32 range - min_uint32 = 0 - max_uint32 = 2**32 - 1 - - # Generate random nonce - nonce = random.randint(min_uint32, max_uint32) - return nonce - - -def raffle_hash_message(user, raffleId, nonce=None, multiplier=None): - - abi = ["address", "uint256"] - data = [Web3.toChecksumAddress(user), raffleId] - if nonce: - abi.append("uint32") - data.append(nonce) - if multiplier: - abi.append("uint256") - data.append(multiplier) - message_hash = Web3().solidityKeccak(abi, data) - hashed_message = encode_defunct(hexstr=message_hash.hex()) - - return hashed_message - - -def sign_hashed_message(hashed_message, private_key): - account = Account.from_key(private_key) - - signed_message = account.sign_message(hashed_message) - - return signed_message.signature.hex() \ No newline at end of file diff --git a/prizetap/validators.py b/prizetap/validators.py index df5cb586..1bd825fa 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -1,20 +1,10 @@ +import json from .models import RaffleEntry -from faucet.faucet_manager.credit_strategy import WeeklyCreditStrategy -from faucet.models import GlobalSettings from rest_framework.exceptions import PermissionDenied -from authentication.models import NetworkTypes, UserProfile +from authentication.models import UserProfile from .models import RaffleEntry, Raffle from .constraints import * -def has_weekly_credit_left(user_profile): - return ( - RaffleEntry.objects.filter( - user_profile=user_profile, - created_at__gte=WeeklyCreditStrategy.get_last_monday(), - ).count() - < GlobalSettings.objects.first().prizetap_weekly_claim_limit - ) - class RaffleEnrollmentValidator: def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs['user_profile'] @@ -27,17 +17,27 @@ def can_enroll_in_raffle(self): ) def check_user_constraints(self): + try: + param_values = json.loads(self.raffle.constraint_params) + except: + param_values = {} for c in self.raffle.constraints.all(): constraint: ConstraintVerification = eval(c.name)(self.user_profile) - if not constraint.is_observed(): + constraint.response = c.response + try: + constraint.set_param_values(param_values[c.name]) + except KeyError: + pass + if not constraint.is_observed(self.raffle.constraint_params): raise PermissionDenied( - constraint.response() + constraint.response ) def check_user_has_wallet(self): - if not self.user_profile.wallets.filter(wallet_type=NetworkTypes.EVM).exists(): + if not self.user_profile.wallets.filter( + wallet_type=self.raffle.chain.chain_type).exists(): raise PermissionDenied( - "You have not connected an EVM wallet to your account" + f"You have not connected an {self.raffle.chain.chain_type} wallet to your account" ) def is_valid(self, data): @@ -47,27 +47,6 @@ def is_valid(self, data): self.check_user_has_wallet() - -class ClaimPrizeValidator: - def __init__(self, *args, **kwargs): - self.user_profile: UserProfile = kwargs['user_profile'] - self.raffle: Raffle = kwargs['raffle'] - - def can_claim_prize(self): - if not self.raffle.is_expired: - raise PermissionDenied( - "The raffle is not over" - ) - if not self.raffle.winner or self.raffle.winner != self.user_profile: - raise PermissionDenied( - "You are not the raffle winner" - ) - - def is_valid(self, data): - self.can_claim_prize() - - - class SetRaffleEntryTxValidator: def __init__(self, *args, **kwargs): diff --git a/prizetap/views.py b/prizetap/views.py index eb3a7077..a9496393 100644 --- a/prizetap/views.py +++ b/prizetap/views.py @@ -3,18 +3,14 @@ from rest_framework.generics import ListAPIView,CreateAPIView from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from authentication.models import NetworkTypes from .models import Raffle, RaffleEntry from .serializers import RaffleSerializer, RaffleEntrySerializer from .validators import ( RaffleEnrollmentValidator, - ClaimPrizeValidator, SetRaffleEntryTxValidator, SetClaimingPrizeTxValidator ) -from permissions.models import Permission - class RaffleListView(ListAPIView): queryset = Raffle.objects.filter(is_active=True).order_by("pk") @@ -50,46 +46,15 @@ def post(self, request, pk): user_profile=user_profile, raffle=raffle, ) - raffle_entry.signature = raffle.generate_signature( - user_profile.wallets.get(wallet_type=NetworkTypes.EVM).address, - raffle_entry.pk, - 1 - ) raffle_entry.save() return Response( { - "detail": "Signature Created Successfully", + "detail": "Enrolled Successfully", "signature": RaffleEntrySerializer(raffle_entry).data, }, status=200, ) - -class ClaimPrizeView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request, pk): - user_profile = request.user.profile - raffle = get_object_or_404(Raffle, pk=pk) - - validator = ClaimPrizeValidator( - user_profile=user_profile, - raffle=raffle - ) - - validator.is_valid(self.request.data) - - signature = raffle.generate_signature( - user_profile.wallets.get(wallet_type=NetworkTypes.EVM).address) - - return Response( - { - "detail": "Signature created successfully", - "success": True, - "signature": signature - }, - status=200, - ) class SetEnrollmentTxView(APIView): permission_classes = [IsAuthenticated] @@ -146,3 +111,15 @@ def post(self, request, pk): }, status=200, ) + +class GetRaffleEntryView(APIView): + def get(self, request, pk): + raffle_entry = get_object_or_404(RaffleEntry, pk=pk) + + return Response( + { + "success": True, + "entry": RaffleEntrySerializer(raffle_entry).data + }, + status=200, + ) \ No newline at end of file diff --git a/tokenTap/serializers.py b/tokenTap/serializers.py index 02d4f75e..72d23674 100644 --- a/tokenTap/serializers.py +++ b/tokenTap/serializers.py @@ -1,17 +1,21 @@ from faucet.serializers import SmallChainSerializer from rest_framework import serializers from core.serializers import UserConstraintBaseSerializer -from tokenTap.models import ( - TokenDistribution, - TokenDistributionClaim, - Constraint -) +from core.models import UserConstraint +from tokenTap.models import TokenDistribution, TokenDistributionClaim, Constraint +from .constraints import * + class ConstraintSerializer(UserConstraintBaseSerializer, serializers.ModelSerializer): class Meta(UserConstraintBaseSerializer.Meta): ref_name = "TokenDistributionConstraint" model = Constraint - + + def get_params(self, constraint: UserConstraint): + c_class: ConstraintVerification = eval(constraint.name) + return [p.value for p in c_class.param_keys()] + + class DetailResponseSerializer(serializers.Serializer): detail = serializers.CharField() @@ -77,6 +81,7 @@ class Meta: "deadline", "max_number_of_claims", "notes", + "token_image_url", ] diff --git a/tokenTap/views.py b/tokenTap/views.py index 7bcd42a7..756feaa4 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -54,8 +54,9 @@ 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(): constraint: ConstraintVerification = eval(c.name)(user_profile) + constraint.response = c.response if not constraint.is_observed(token_distribution=token_distribution): - raise PermissionDenied(constraint.response()) + raise PermissionDenied(constraint.response) def check_user_weekly_credit(self, user_profile): if not has_weekly_credit_left(user_profile):