diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..13dbf2d6 Binary files /dev/null and b/.DS_Store differ diff --git a/authentication/serializers.py b/authentication/serializers.py index b3359f37..0772ddda 100644 --- a/authentication/serializers.py +++ b/authentication/serializers.py @@ -1,12 +1,8 @@ -from django.db import IntegrityError -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 rest_framework.authtoken.models import Token +from authentication.models import UserProfile, Wallet +from faucet.faucet_manager.claim_manager import LimitedChainClaimManager from faucet.models import GlobalSettings @@ -57,7 +53,7 @@ class Meta: class ProfileSerializer(serializers.ModelSerializer): wallets = WalletSerializer(many=True, read_only=True) - total_weekly_claims_remaining = serializers.SerializerMethodField() + total_round_claims_remaining = serializers.SerializerMethodField() token = serializers.SerializerMethodField() class Meta: @@ -69,7 +65,7 @@ class Meta: "initial_context_id", "is_meet_verified", "is_aura_verified", - "total_weekly_claims_remaining", + "total_round_claims_remaining", "wallets", ] @@ -77,14 +73,12 @@ def get_token(self, instance): token, bol = Token.objects.get_or_create(user=instance.user) return token.key - def get_total_weekly_claims_remaining(self, instance): + def get_total_round_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) - ) - + return gs.gastap_round_claim_limit - LimitedChainClaimManager.get_total_round_claims(instance) + + class SimpleProfilerSerializer(serializers.ModelSerializer): wallets = WalletSerializer(many=True, read_only=True) username = serializers.SerializerMethodField() @@ -102,4 +96,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/brightIDfaucet/celery.py b/brightIDfaucet/celery.py index 7f23cd70..bfa25893 100644 --- a/brightIDfaucet/celery.py +++ b/brightIDfaucet/celery.py @@ -36,7 +36,7 @@ }, "update_tokentap_claim_for_verified_lightning_claims": { "task": "faucet.tasks.update_tokentap_claim_for_verified_lightning_claims", - "schedule": 3, + "schedule": 9, }, "update-tokens-price": { "task": "faucet.tasks.update_tokens_price", diff --git a/core/migrations/0003_alter_tokenprice_price_url.py b/core/migrations/0003_alter_tokenprice_price_url.py new file mode 100644 index 00000000..45d1607c --- /dev/null +++ b/core/migrations/0003_alter_tokenprice_price_url.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-10-27 08:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_alter_tokenprice_price_url'), + ] + + operations = [ + migrations.AlterField( + model_name='tokenprice', + name='price_url', + field=models.URLField(blank=True, max_length=255, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index 7dcca52e..09a38cec 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from .constraints import * + +from .constraints import BrightIDAuraVerification, BrightIDMeetVerification class UserConstraint(models.Model): @@ -19,9 +20,7 @@ class Type(models.TextChoices): choices=[(c.__name__, c.__name__) for c in constraints], ) title = models.CharField(max_length=255) - type = models.CharField( - max_length=10, choices=Type.choices, default=Type.VERIFICATION - ) + type = models.CharField(max_length=10, choices=Type.choices, default=Type.VERIFICATION) description = models.TextField(null=True, blank=True) response = models.TextField(null=True, blank=True) icon_url = models.CharField(max_length=255, null=True, blank=True) @@ -42,10 +41,8 @@ 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, null=True) - symbol = models.CharField( - max_length=255, db_index=True, unique=True, null=False, blank=False - ) + price_url = models.URLField(max_length=255, null=True, blank=True) + symbol = models.CharField(max_length=255, db_index=True, unique=True, null=False, blank=False) class BigNumField(models.Field): diff --git a/core/utils.py b/core/utils.py index 21879d7e..98916608 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,47 +1,46 @@ import datetime + import pytz -from time import time from web3 import Web3 -from web3.middleware import geth_poa_middleware from web3.contract.contract import Contract, ContractFunction -from web3.types import Type, TxParams -from django.utils import timezone +from web3.middleware import geth_poa_middleware +from web3.types import TxParams, Type class TimeUtils: - @staticmethod - def get_last_monday(): - now = int(time()) - day = 86400 # seconds in a day - week = 7 * day - weeks = now // week # number of weeks since epoch - monday = 345600 # first monday midnight - last_monday_midnight = monday + (weeks * week) - - # last monday could be off by one week - if last_monday_midnight > now: - last_monday_midnight -= week - - return timezone.make_aware( - datetime.datetime.fromtimestamp(last_monday_midnight) - ) - - @staticmethod - def get_second_last_monday(): - now = int(time()) - day = 86400 # seconds in a day - week = 7 * day - weeks = now // week # number of weeks since epoch - monday = 345600 # first monday midnight - last_monday_midnight = monday + (weeks * week) - - # last monday could be off by one week - if last_monday_midnight > now: - last_monday_midnight -= week - - return timezone.make_aware( - datetime.datetime.fromtimestamp(last_monday_midnight - week) - ) + # @staticmethod + # def get_last_monday(): + # now = int(time()) + # day = 86400 # seconds in a day + # week = 7 * day + # weeks = now // week # number of weeks since epoch + # monday = 345600 # first monday midnight + # last_monday_midnight = monday + (weeks * week) + + # # last monday could be off by one week + # if last_monday_midnight > now: + # last_monday_midnight -= week + + # return timezone.make_aware( + # datetime.datetime.fromtimestamp(last_monday_midnight) + # ) + + # @staticmethod + # def get_second_last_monday(): + # now = int(time()) + # day = 86400 # seconds in a day + # week = 7 * day + # weeks = now // week # number of weeks since epoch + # monday = 345600 # first monday midnight + # last_monday_midnight = monday + (weeks * week) + + # # last monday could be off by one week + # if last_monday_midnight > now: + # last_monday_midnight -= week + + # return timezone.make_aware( + # datetime.datetime.fromtimestamp(last_monday_midnight - week) + # ) @staticmethod def get_first_day_of_the_month(): @@ -49,9 +48,17 @@ def get_first_day_of_the_month(): first_day = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) return first_day + @staticmethod + def get_first_day_of_last_month(): + now = datetime.datetime.now(pytz.timezone("UTC")) + first_day = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + last_month = first_day - datetime.timedelta(days=1) + first_day_of_last_month = last_month.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + return first_day_of_last_month + class Web3Utils: - def __init__(self, rpc_url, poa = False) -> None: + def __init__(self, rpc_url, poa=False) -> None: self._rpc_url = rpc_url self._w3 = None self._account = None @@ -71,7 +78,7 @@ def w3(self) -> Web3: return self._w3 raise Exception(f"RPC provider is not connected ({self._rpc_url})") - + @property def poa(self): return self._poa @@ -102,8 +109,7 @@ def contract_call(self, func: Type[ContractFunction], from_address=None): def build_contract_txn(self, func: Type[ContractFunction]): nonce = self.w3.eth.get_transaction_count(self.account.address) - tx_data = func.build_transaction( - {"from": self.account.address, "nonce": nonce}) + tx_data = func.build_transaction({"from": self.account.address, "nonce": nonce}) return self.sign_tx(tx_data) def sign_tx(self, tx_data: TxParams): @@ -117,6 +123,6 @@ def wait_for_transaction_receipt(self, tx_hash): def current_block(self): return self.w3.eth.block_number - + def get_transaction_by_hash(self, hash): return self.w3.eth.get_transaction(hash) diff --git a/faucet/admin.py b/faucet/admin.py index ccb83759..e95f9b50 100644 --- a/faucet/admin.py +++ b/faucet/admin.py @@ -1,5 +1,15 @@ from django.contrib import admin -from .models import * + +from .models import ( + BrightUser, + Chain, + ClaimReceipt, + DonationReceipt, + GlobalSettings, + LightningConfig, + TransactionBatch, + WalletAccount, +) class ChainAdmin(admin.ModelAdmin): @@ -28,8 +38,8 @@ def last_updated_with_seconds(obj): class TXHashFilter(admin.SimpleListFilter): - title = 'has tx hash' # or use _('country') for translated title - parameter_name = 'has_tx_hash' + title = "has tx hash" # or use _('country') for translated title + parameter_name = "has_tx_hash" def lookups(self, request, model_admin): return ( @@ -66,8 +76,8 @@ class WalletAccountAdmin(admin.ModelAdmin): class GlobalSettingsAdmin(admin.ModelAdmin): - list_display = ["pk", "weekly_chain_claim_limit", "tokentap_weekly_claim_limit"] - list_editable = ["weekly_chain_claim_limit", "tokentap_weekly_claim_limit"] + list_display = ["pk", "gastap_round_claim_limit", "tokentap_round_claim_limit"] + list_editable = ["gastap_round_claim_limit", "tokentap_round_claim_limit"] class TransactionBatchAdmin(admin.ModelAdmin): @@ -92,16 +102,9 @@ class LightningConfigAdmin(admin.ModelAdmin): class DonationReceiptAdmin(admin.ModelAdmin): - list_display = [ - 'tx_hash', - 'user_profile', - 'chain', - 'value', - 'total_price', - 'datetime' - ] - search_fields = ['tx_hash'] - list_filter = ['chain', 'user_profile'] + 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) diff --git a/faucet/constraints.py b/faucet/constraints.py index b21c75d2..6164305d 100644 --- a/faucet/constraints.py +++ b/faucet/constraints.py @@ -1,8 +1,12 @@ +import logging + import requests -from core.constraints import * + +from core.constraints import ConstraintParam, ConstraintVerification from core.utils import Web3Utils -from faucet.faucet_manager.credit_strategy import WeeklyCreditStrategy -from .models import DonationReceipt, Chain, ClaimReceipt +from faucet.faucet_manager.credit_strategy import RoundCreditStrategy + +from .models import Chain, ClaimReceipt, DonationReceipt class DonationConstraint(ConstraintVerification): @@ -24,7 +28,8 @@ class OptimismDonationConstraint(DonationConstraint): def is_observed(self, *args, **kwargs): try: chain = Chain.objects.get(chain_id=10) - except: + except Exception as e: + logging.error(e) return False self._param_values[ConstraintParam.CHAIN] = chain.pk return super().is_observed(*args, **kwargs) @@ -38,12 +43,11 @@ def is_observed(self, *args, **kwargs): chain = Chain.objects.get(pk=chain_pk) w3 = Web3Utils(chain.rpc_url_private, chain.poa) current_block = w3.current_block() - user_address = self.user_profile.wallets.get( - wallet_type=chain.chain_type - ).address + user_address = self.user_profile.wallets.get(wallet_type=chain.chain_type).address first_internal_tx = requests.get( - f"{chain.explorer_api_url}/api?module=account&action=txlistinternal&address={user_address}&startblock=0&endblock={current_block}&page=1&offset=1&sort=asc&apikey={chain.explorer_api_key}" + f"{chain.explorer_api_url}/api?module=account&action=txlistinternal&address={user_address}&start" + f"block=0&endblock={current_block}&page=1&offset=1&sort=asc&apikey={chain.explorer_api_key}" ) first_internal_tx = first_internal_tx.json() if first_internal_tx and first_internal_tx["status"] == "1": @@ -54,16 +58,15 @@ def is_observed(self, *args, **kwargs): and first_internal_tx["isError"] == "0" ): first_tx = requests.get( - f"{chain.explorer_api_url}/api?module=account&action=txlist&address={user_address}&startblock=0&endblock={current_block}&page=1&offset=1&sort=asc&apikey={chain.explorer_api_key}" + f"{chain.explorer_api_url}/api?module=account&action=txlist&address={user_address}&startblock=0&" + f"endblock={current_block}&page=1&offset=1&sort=asc&apikey={chain.explorer_api_key}" ) first_tx = first_tx.json() if first_tx: if not first_tx["result"]: return True first_tx = first_tx["result"][0] - claiming_gas_tx = w3.get_transaction_by_hash( - first_internal_tx["hash"] - ) + claiming_gas_tx = w3.get_transaction_by_hash(first_internal_tx["hash"]) web3_first_tx = w3.get_transaction_by_hash(first_tx["hash"]) return web3_first_tx["blockNumber"] > claiming_gas_tx["blockNumber"] return False @@ -75,7 +78,8 @@ class OptimismClaimingGasConstraint(EvmClaimingGasConstraint): def is_observed(self, *args, **kwargs): try: chain = Chain.objects.get(chain_id=10) - except: + except Exception as e: + logging.error(e) return False self._param_values[ConstraintParam.CHAIN] = chain.pk return super().is_observed(*args, **kwargs) @@ -91,7 +95,7 @@ def is_observed(self, *args, **kwargs): user_profile=self.user_profile, chain=chain, _status=ClaimReceipt.VERIFIED, - datetime__gte=WeeklyCreditStrategy.get_last_monday(), + datetime__gte=RoundCreditStrategy.get_start_of_the_round(), ).exists() @@ -101,7 +105,8 @@ class OptimismHasClaimedGasInThisRound(HasClaimedGasInThisRound): def is_observed(self, *args, **kwargs): try: chain = Chain.objects.get(chain_id=10) - except: + except Exception as e: + logging.error(e) return False self._param_values[ConstraintParam.CHAIN] = chain.pk return super().is_observed(*args, **kwargs) diff --git a/faucet/faucet_manager/brightid_user_registry.py b/faucet/faucet_manager/brightid_user_registry.py index 50fb9c35..d89c14c8 100644 --- a/faucet/faucet_manager/brightid_user_registry.py +++ b/faucet/faucet_manager/brightid_user_registry.py @@ -1,40 +1,40 @@ -from eth_account.signers.local import LocalAccount -from web3 import Web3 -from web3.exceptions import TimeExhausted -from web3.gas_strategies.rpc import rpc_gas_price_strategy -from web3.middleware import geth_poa_middleware -from faucet.faucet_manager.brightid_user_registry_abi import bright_id_user_registry_abi -from faucet.models import Chain, BrightUser +# from eth_account.signers.local import LocalAccount +# from web3 import Web3 +# from web3.exceptions import TimeExhausted +# from web3.gas_strategies.rpc import rpc_gas_price_strategy +# from web3.middleware import geth_poa_middleware +# from faucet.faucet_manager.brightid_user_registry_abi import bright_id_user_registry_abi +# from faucet.models import Chain, BrightUser -class BrightIdUserRegistry: - def __init__(self, chain: Chain, bright_id_user_registry_address: str): - self.chain = chain - self.abi = bright_id_user_registry_abi - self.bright_id_user_registry_address = bright_id_user_registry_address +# class BrightIdUserRegistry: +# def __init__(self, chain: Chain, bright_id_user_registry_address: str): +# self.chain = chain +# self.abi = bright_id_user_registry_abi +# self.bright_id_user_registry_address = bright_id_user_registry_address - @property - def w3(self) -> Web3: - assert self.chain.rpc_url_private is not None - _w3 = Web3(Web3.HTTPProvider(self.chain.rpc_url_private)) - if self.chain.poa: - _w3.middleware_onion.inject(geth_poa_middleware, layer=0) - if _w3.isConnected(): - _w3.eth.set_gas_price_strategy(rpc_gas_price_strategy) - return _w3 - raise Exception(f"Could not connect to rpc {self.chain.rpc_url_private}") +# @property +# def w3(self) -> Web3: +# assert self.chain.rpc_url_private is not None +# _w3 = Web3(Web3.HTTPProvider(self.chain.rpc_url_private)) +# if self.chain.poa: +# _w3.middleware_onion.inject(geth_poa_middleware, layer=0) +# if _w3.isConnected(): +# _w3.eth.set_gas_price_strategy(rpc_gas_price_strategy) +# return _w3 +# raise Exception(f"Could not connect to rpc {self.chain.rpc_url_private}") - @property - def contract(self): - return self.w3.eth.contract( - address=self.get_checksum_address(self.bright_id_user_registry_address), - abi=self.abi, - ) +# @property +# def contract(self): +# return self.w3.eth.contract( +# address=self.get_checksum_address(self.bright_id_user_registry_address), +# abi=self.abi, +# ) - def get_checksum_address(self, address): - return Web3.toChecksumAddress(address.lower()) +# def get_checksum_address(self, address): +# return Web3.toChecksumAddress(address.lower()) - def is_verified_user(self, address): - return self.contract.functions.isVerifiedUser( - self.get_checksum_address(address) - ).call() +# def is_verified_user(self, address): +# return self.contract.functions.isVerifiedUser( +# self.get_checksum_address(address) +# ).call() diff --git a/faucet/faucet_manager/claim_manager.py b/faucet/faucet_manager/claim_manager.py index 75ca3951..d445dc07 100644 --- a/faucet/faucet_manager/claim_manager.py +++ b/faucet/faucet_manager/claim_manager.py @@ -1,19 +1,19 @@ -import logging import abc +import logging from abc import ABC + +from django.db import transaction from django.utils import timezone -from authentication.models import UserProfile -from authentication.models import NetworkTypes -from faucet.faucet_manager.lnpay_client import LNPayClient +from authentication.models import NetworkTypes, UserProfile from faucet.faucet_manager.credit_strategy import ( CreditStrategy, CreditStrategyFactory, - WeeklyCreditStrategy, + RoundCreditStrategy, ) from faucet.faucet_manager.fund_manager import EVMFundManager -from faucet.models import ClaimReceipt, BrightUser, GlobalSettings -from django.db import transaction +from faucet.faucet_manager.lnpay_client import LNPayClient +from faucet.models import BrightUser, ClaimReceipt, GlobalSettings class ClaimManager(ABC): @@ -36,9 +36,7 @@ def fund_manager(self): def claim(self, amount, passive_address=None): with transaction.atomic(): - user_profile = UserProfile.objects.select_for_update().get( - pk=self.credit_strategy.user_profile.pk - ) + user_profile = UserProfile.objects.select_for_update().get(pk=self.credit_strategy.user_profile.pk) self.assert_pre_claim_conditions(amount, user_profile) return self.create_pending_claim_receipt( amount, passive_address @@ -46,8 +44,7 @@ def claim(self, amount, passive_address=None): def assert_pre_claim_conditions(self, amount, user_profile): assert amount <= self.credit_strategy.get_unclaimed() - # TODO: uncomment this - assert self.user_is_meet_verified() == True + assert self.user_is_meet_verified() is True assert not ClaimReceipt.objects.filter( chain=self.credit_strategy.chain, user_profile=user_profile, @@ -72,13 +69,13 @@ def user_is_meet_verified(self) -> bool: class LimitedChainClaimManager(SimpleClaimManager): - def get_weekly_limit(self): - limit = GlobalSettings.objects.first().weekly_chain_claim_limit + def get_round_limit(self): + limit = GlobalSettings.objects.first().gastap_round_claim_limit return limit @staticmethod - def get_total_weekly_claims(user_profile): - last_monday = WeeklyCreditStrategy.get_last_monday() + def get_total_round_claims(user_profile): + start_of_the_round = RoundCreditStrategy.get_start_of_the_round() return ClaimReceipt.objects.filter( user_profile=user_profile, _status__in=[ @@ -87,27 +84,28 @@ def get_total_weekly_claims(user_profile): BrightUser.PENDING, BrightUser.VERIFIED, ], - datetime__gte=last_monday, + datetime__gte=start_of_the_round, ).count() def assert_pre_claim_conditions(self, amount, user_profile): super().assert_pre_claim_conditions(amount, user_profile) - total_claims = self.get_total_weekly_claims(user_profile) - assert total_claims < self.get_weekly_limit() + total_claims = self.get_total_round_claims(user_profile) + assert total_claims < self.get_round_limit() + class LightningClaimManger(LimitedChainClaimManager): def claim(self, amount, passive_address): try: lnpay_client = LNPayClient( - self.credit_strategy.chain.rpc_url_private, - self.credit_strategy.chain.wallet.main_key, - self.credit_strategy.chain.fund_manager_address + self.credit_strategy.chain.rpc_url_private, + self.credit_strategy.chain.wallet.main_key, + self.credit_strategy.chain.fund_manager_address, ) decoded_invoice = lnpay_client.decode_invoice(passive_address) except Exception as e: logging.error(e) raise AssertionError("Could not decode the invoice") - assert int(decoded_invoice['num_satoshis']) == amount, "Invalid amount" + assert int(decoded_invoice["num_satoshis"]) == amount, "Invalid amount" return super().claim(amount, passive_address) diff --git a/faucet/faucet_manager/credit_strategy.py b/faucet/faucet_manager/credit_strategy.py index 1fb51ecd..261c9548 100644 --- a/faucet/faucet_manager/credit_strategy.py +++ b/faucet/faucet_manager/credit_strategy.py @@ -1,16 +1,12 @@ import abc -from abc import ABC -from time import time import datetime +from abc import ABC -from django.db.models import Sum -from django.utils import timezone import pytz -from authentication.models import UserProfile +from django.db.models import Sum -from brightIDfaucet import settings -from faucet.faucet_manager.brightid_user_registry import BrightIdUserRegistry -from faucet.models import ClaimReceipt, BrightUser, Chain +from authentication.models import UserProfile +from faucet.models import Chain, ClaimReceipt class CreditStrategy(ABC): @@ -51,12 +47,68 @@ def get_claimed(self): return _sum def get_unclaimed(self): - # print("max_claim_amount", self.chain.max_claim_amount) - # print("get_claimed", self.get_claimed()) return int(self.chain.max_claim_amount) - int(self.get_claimed()) -class WeeklyCreditStrategy(SimpleCreditStrategy): +# class WeeklyCreditStrategy(SimpleCreditStrategy): +# def __int__(self, chain: Chain, user_profile: UserProfile): +# self.chain = chain +# self.user_profile = user_profile + +# def get_claim_receipts(self): +# return ClaimReceipt.objects.filter( +# chain=self.chain, +# user_profile=self.user_profile, +# _status=ClaimReceipt.VERIFIED, +# datetime__gte=self.get_last_monday(), +# ) + +# @staticmethod +# def get_last_monday(): +# now = int(time()) +# day = 86400 # seconds in a day +# week = 7 * day +# weeks = now // week # number of weeks since epoch +# monday = 345600 # first monday midnight +# last_monday_midnight = monday + (weeks * week) + +# # last monday could be off by one week +# if last_monday_midnight > now: +# last_monday_midnight -= week + +# return timezone.make_aware(datetime.datetime.fromtimestamp(last_monday_midnight)) + +# @staticmethod +# def get_second_last_monday(): +# now = int(time()) +# day = 86400 # seconds in a day +# week = 7 * day +# weeks = now // week # number of weeks since epoch +# monday = 345600 # first monday midnight +# last_monday_midnight = monday + (weeks * week) + +# # last monday could be off by one week +# if last_monday_midnight > now: +# last_monday_midnight -= week + +# return timezone.make_aware(datetime.datetime.fromtimestamp(last_monday_midnight - week)) + + +# class ArbitrumCreditStrategy(WeeklyCreditStrategy): +# def get_unclaimed(self): +# contract_address = "0x631a12430F94207De980D9b6A744AEB4093DCeC1" +# max_claim_amount = self.chain.max_claim_amount +# is_verified_user = BrightIdUserRegistry(self.chain, contract_address).is_verified_user( +# self.user_profile.initial_context_id +# ) + +# if is_verified_user: +# max_claim_amount = 5000000000000000 + +# return max_claim_amount - self.get_claimed() + + +class RoundCreditStrategy(SimpleCreditStrategy): def __int__(self, chain: Chain, user_profile: UserProfile): self.chain = chain self.user_profile = user_profile @@ -66,62 +118,30 @@ def get_claim_receipts(self): chain=self.chain, user_profile=self.user_profile, _status=ClaimReceipt.VERIFIED, - datetime__gte=self.get_last_monday(), + datetime__gte=self._get_first_day_of_the_month(), ) @staticmethod - def get_last_monday(): - now = int(time()) - day = 86400 # seconds in a day - week = 7 * day - weeks = now // week # number of weeks since epoch - monday = 345600 # first monday midnight - last_monday_midnight = monday + (weeks * week) - - # last monday could be off by one week - if last_monday_midnight > now: - last_monday_midnight -= week - - return timezone.make_aware( - datetime.datetime.fromtimestamp(last_monday_midnight) - ) + def get_start_of_the_round(): + return RoundCreditStrategy._get_first_day_of_the_month() @staticmethod - def get_second_last_monday(): - now = int(time()) - day = 86400 # seconds in a day - week = 7 * day - weeks = now // week # number of weeks since epoch - monday = 345600 # first monday midnight - last_monday_midnight = monday + (weeks * week) - - # last monday could be off by one week - if last_monday_midnight > now: - last_monday_midnight -= week - - return timezone.make_aware( - datetime.datetime.fromtimestamp(last_monday_midnight - week) - ) + def get_start_of_previous_round(): + return RoundCreditStrategy._get_first_day_of_last_month() - @staticmethod - def get_first_day_of_the_month(): + @classmethod + def _get_first_day_of_the_month(cls): now = datetime.datetime.now(pytz.timezone("UTC")) first_day = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) return first_day - -class ArbitrumCreditStrategy(WeeklyCreditStrategy): - def get_unclaimed(self): - contract_address = "0x631a12430F94207De980D9b6A744AEB4093DCeC1" - max_claim_amount = self.chain.max_claim_amount - is_verified_user = BrightIdUserRegistry( - self.chain, contract_address - ).is_verified_user(self.user_profile.initial_context_id) - - if is_verified_user: - max_claim_amount = 5000000000000000 - - return max_claim_amount - self.get_claimed() + @classmethod + def _get_first_day_of_last_month(cls): + now = datetime.datetime.now(pytz.timezone("UTC")) + first_day = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + last_month = first_day - datetime.timedelta(days=1) + first_day_of_last_month = last_month.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + return first_day_of_last_month class CreditStrategyFactory: @@ -130,9 +150,7 @@ def __init__(self, chain, user_profile): self.user_profile = user_profile def get_strategy_class(self): - return WeeklyCreditStrategy - if self.chain.chain_id == "42161": - return ArbitrumCreditStrategy + return RoundCreditStrategy def get_strategy(self) -> CreditStrategy: _Strategy = self.get_strategy_class() diff --git a/faucet/faucet_manager/fund_manager.py b/faucet/faucet_manager/fund_manager.py index 5df163ad..e35e9909 100644 --- a/faucet/faucet_manager/fund_manager.py +++ b/faucet/faucet_manager/fund_manager.py @@ -1,15 +1,9 @@ -import time -import os import logging +import os +import time + from django.core.cache import cache from eth_account.signers.local import LocalAccount -from web3 import Web3 -from web3.gas_strategies.rpc import rpc_gas_price_strategy -from web3.middleware import geth_poa_middleware -from faucet.faucet_manager.fund_manager_abi import manager_abi -from faucet.models import Chain, BrightUser, LightningConfig -from faucet.helpers import memcache_lock -from faucet.constants import * from solana.rpc.api import Client from solana.rpc.core import RPCException, RPCNoResultException from solana.transaction import Transaction @@ -17,10 +11,19 @@ from solders.pubkey import Pubkey from solders.signature import Signature from solders.transaction_status import TransactionConfirmationStatus -from .anchor_client.accounts.lock_account import LockAccount +from web3 import Web3 +from web3.gas_strategies.rpc import rpc_gas_price_strategy +from web3.middleware import geth_poa_middleware + +from faucet.constants import MEMCACHE_LIGHTNING_LOCK_KEY +from faucet.faucet_manager.fund_manager_abi import manager_abi +from faucet.helpers import memcache_lock +from faucet.models import BrightUser, Chain, LightningConfig + from .anchor_client import instructions -from .solana_client import SolanaClient +from .anchor_client.accounts.lock_account import LockAccount from .lnpay_client import LNPayClient +from .solana_client import SolanaClient class FundMangerException: @@ -48,9 +51,7 @@ def w3(self) -> Web3: return _w3 except Exception as e: logging.error(e) - raise FundMangerException.RPCError( - f"Could not connect to rpc {self.chain.rpc_url_private}" - ) + raise FundMangerException.RPCError(f"Could not connect to rpc {self.chain.rpc_url_private}") @property def is_gas_price_too_high(self): @@ -77,9 +78,7 @@ def get_fund_manager_checksum_address(self): @property def contract(self): - return self.w3.eth.contract( - address=self.get_fund_manager_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) @@ -153,9 +152,7 @@ def w3(self) -> Client: return _w3 except Exception as e: logging.error(e) - raise FundMangerException.RPCError( - f"Could not connect to rpc {self.chain.rpc_url_private}" - ) + raise FundMangerException.RPCError(f"Could not connect to rpc {self.chain.rpc_url_private}") @property def account(self) -> Keypair: @@ -171,9 +168,7 @@ def lock_account_seed(self) -> bytes: @property def lock_account_address(self) -> Pubkey: - lock_account_address, nonce = Pubkey.find_program_address( - [self.lock_account_seed], self.program_id - ) + lock_account_address, nonce = Pubkey.find_program_address([self.lock_account_seed], self.program_id) return lock_account_address @property @@ -247,23 +242,17 @@ def multi_transfer(self, data): def is_tx_verified(self, tx_hash): try: confirmation_status = ( - self.w3.get_signature_statuses([Signature.from_string(tx_hash)]) - .value[0] - .confirmation_status + self.w3.get_signature_statuses([Signature.from_string(tx_hash)]).value[0].confirmation_status ) return confirmation_status in [ TransactionConfirmationStatus.Confirmed, TransactionConfirmationStatus.Finalized, ] except RPCException: - logging.warning( - "Solana raised the RPCException at get_signature_statuses()" - ) + logging.warning("Solana raised the RPCException at get_signature_statuses()") return False except RPCNoResultException: - logging.warning( - "Solana raised the RPCNoResultException at get_signature_statuses()" - ) + logging.warning("Solana raised the RPCNoResultException at get_signature_statuses()") return False except Exception: raise @@ -285,9 +274,7 @@ def api_key(self): @property def lnpay_client(self): - return LNPayClient( - self.chain.rpc_url_private, self.api_key, self.chain.fund_manager_address - ) + return LNPayClient(self.chain.rpc_url_private, self.api_key, self.chain.fund_manager_address) def __check_max_cap_exceeds(self, amount) -> bool: try: @@ -309,9 +296,7 @@ def multi_transfer(self, data): assert acquired, "Could not acquire Lightning multi-transfer lock" item = data[0] - assert not self.__check_max_cap_exceeds( - item["amount"] - ), "Lightning periodic max cap exceeded" + assert not self.__check_max_cap_exceeds(item["amount"]), "Lightning periodic max cap exceeded" try: pay_result = client.pay_invoice(item["to"]) diff --git a/faucet/migrations/0060_rename_weekly_chain_claim_limit_globalsettings_gastap_claim_limit.py b/faucet/migrations/0060_rename_weekly_chain_claim_limit_globalsettings_gastap_claim_limit.py new file mode 100644 index 00000000..553c8071 --- /dev/null +++ b/faucet/migrations/0060_rename_weekly_chain_claim_limit_globalsettings_gastap_claim_limit.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-10-24 13:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0059_chain_explorer_api_key_chain_explorer_api_url'), + ] + + operations = [ + migrations.RenameField( + model_name='globalsettings', + old_name='weekly_chain_claim_limit', + new_name='gastap_claim_limit', + ), + ] diff --git a/faucet/migrations/0061_rename_gastap_claim_limit_globalsettings_gastap_round_claim_limit.py b/faucet/migrations/0061_rename_gastap_claim_limit_globalsettings_gastap_round_claim_limit.py new file mode 100644 index 00000000..ab52234f --- /dev/null +++ b/faucet/migrations/0061_rename_gastap_claim_limit_globalsettings_gastap_round_claim_limit.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-10-24 13:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0060_rename_weekly_chain_claim_limit_globalsettings_gastap_claim_limit'), + ] + + operations = [ + migrations.RenameField( + model_name='globalsettings', + old_name='gastap_claim_limit', + new_name='gastap_round_claim_limit', + ), + ] diff --git a/faucet/migrations/0062_rename_tokentap_weekly_claim_limit_globalsettings_tokentap_round_claim_limit.py b/faucet/migrations/0062_rename_tokentap_weekly_claim_limit_globalsettings_tokentap_round_claim_limit.py new file mode 100644 index 00000000..fe8c1d4a --- /dev/null +++ b/faucet/migrations/0062_rename_tokentap_weekly_claim_limit_globalsettings_tokentap_round_claim_limit.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-10-24 13:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0061_rename_gastap_claim_limit_globalsettings_gastap_round_claim_limit'), + ] + + operations = [ + migrations.RenameField( + model_name='globalsettings', + old_name='tokentap_weekly_claim_limit', + new_name='tokentap_round_claim_limit', + ), + ] diff --git a/faucet/migrations/0063_rename_prizetap_weekly_claim_limit_globalsettings_prizetap_round_claim_limit.py b/faucet/migrations/0063_rename_prizetap_weekly_claim_limit_globalsettings_prizetap_round_claim_limit.py new file mode 100644 index 00000000..f4ffffbe --- /dev/null +++ b/faucet/migrations/0063_rename_prizetap_weekly_claim_limit_globalsettings_prizetap_round_claim_limit.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-10-24 13:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0062_rename_tokentap_weekly_claim_limit_globalsettings_tokentap_round_claim_limit'), + ] + + operations = [ + migrations.RenameField( + model_name='globalsettings', + old_name='prizetap_weekly_claim_limit', + new_name='prizetap_round_claim_limit', + ), + ] diff --git a/faucet/models.py b/faucet/models.py index f0fd8d17..affaa022 100644 --- a/faucet/models.py +++ b/faucet/models.py @@ -1,25 +1,21 @@ -from decimal import Decimal -from datetime import datetime, timedelta +import binascii import logging -from django.db import models import uuid +from datetime import datetime, timedelta + +from bip_utils import Bip44, Bip44Coins +from django.conf import settings +from django.core.cache import cache +from django.db import models from django.utils import timezone from encrypted_model_fields.fields import EncryptedCharField -import binascii -from bip_utils import Bip44Coins, Bip44 -from web3.exceptions import TimeExhausted -from django.conf import settings -from authentication.models import NetworkTypes, UserProfile, Wallet -from solders.pubkey import Pubkey from solders.keypair import Keypair -from faucet.faucet_manager.lnpay_client import LNPayClient -from django.core.cache import cache -from core.models import BigNumField +from solders.pubkey import Pubkey +from authentication.models import NetworkTypes, UserProfile from brightIDfaucet.settings import BRIGHT_ID_INTERFACE - -# import django transaction -from django.db import transaction +from core.models import BigNumField +from faucet.faucet_manager.lnpay_client import LNPayClient def get_cache_time(id): @@ -29,22 +25,20 @@ def get_cache_time(id): class WalletAccount(models.Model): name = models.CharField(max_length=255, blank=True, null=True) private_key = EncryptedCharField(max_length=100) - network_type = models.CharField( - choices=NetworkTypes.networks, max_length=10, default=NetworkTypes.EVM - ) + network_type = models.CharField(choices=NetworkTypes.networks, max_length=10, default=NetworkTypes.EVM) @property def address(self): try: - node = Bip44.FromPrivateKey( - binascii.unhexlify(self.private_key), Bip44Coins.ETHEREUM - ) + node = Bip44.FromPrivateKey(binascii.unhexlify(self.private_key), Bip44Coins.ETHEREUM) return node.PublicKey().ToAddress() - except: + except Exception as e: + logging.exception(f"Error getting address for {self.name} error is {e}") try: keypair = Keypair.from_base58_string(self.private_key) return str(keypair.pubkey()) - except: + except Exception as e2: + logging.exception(f"Error getting address for {self.name} error is {e2}") pass def __str__(self) -> str: @@ -83,12 +77,8 @@ class BrightUser(models.Model): address = models.CharField(max_length=45, unique=True) context_id = models.UUIDField(default=uuid.uuid4, unique=True) - _verification_status = models.CharField( - max_length=1, choices=states, default=PENDING - ) - _last_verified_datetime = models.DateTimeField( - default=timezone.make_aware(datetime.utcfromtimestamp(0)) - ) + _verification_status = models.CharField(max_length=1, choices=states, default=PENDING) + _last_verified_datetime = models.DateTimeField(default=timezone.make_aware(datetime.utcfromtimestamp(0))) _sponsored = models.BooleanField(default=False) objects = BrightUserManager() @@ -191,9 +181,7 @@ 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() + count = ClaimReceipt.objects.filter(_status__in=[ClaimReceipt.VERIFIED, BrightUser.VERIFIED]).count() cache.set("gastap_claims_count", count, 600) return count @@ -222,9 +210,7 @@ class Chain(models.Model): fund_manager_address = models.CharField(max_length=255) tokentap_contract_address = models.CharField(max_length=255, null=True, blank=True) - wallet = models.ForeignKey( - WalletAccount, related_name="chains", on_delete=models.PROTECT - ) + wallet = models.ForeignKey(WalletAccount, related_name="chains", on_delete=models.PROTECT) max_gas_price = models.BigIntegerField(default=250000000000) gas_multiplier = models.FloatField(default=1) @@ -232,9 +218,7 @@ class Chain(models.Model): needs_funding = models.BooleanField(default=False) is_testnet = models.BooleanField(default=False) - chain_type = models.CharField( - max_length=10, choices=NetworkTypes.networks, default=NetworkTypes.EVM - ) + chain_type = models.CharField(max_length=10, choices=NetworkTypes.networks, default=NetworkTypes.EVM) order = models.IntegerField(default=0) is_active = models.BooleanField(default=True) @@ -276,9 +260,7 @@ def get_manager_balance(self): if self.chain_type == NetworkTypes.EVM or int(self.chain_id) == 500: if self.chain_id == 500: logging.debug("chain XDC NONEVM is checking its balances") - funds = EVMFundManager(self).w3.eth.get_balance( - self.fund_manager_address - ) + funds = EVMFundManager(self).w3.eth.get_balance(self.fund_manager_address) return funds elif self.chain_type == NetworkTypes.SOLANA: @@ -295,9 +277,7 @@ def get_manager_balance(self): raise Exception("Invalid chain type") except Exception as e: - logging.exception( - f"Error getting manager balance for {self.chain_name} error is {e}" - ) + logging.exception(f"Error getting manager balance for {self.chain_name} error is {e}") return 0 @property @@ -318,9 +298,7 @@ def get_wallet_balance(self): return EVMFundManager(self).w3.eth.get_balance(self.wallet.address) elif self.chain_type == NetworkTypes.SOLANA: fund_manager = SolanaFundManager(self) - v = fund_manager.w3.get_balance( - Pubkey.from_string(self.wallet.address) - ).value + v = fund_manager.w3.get_balance(Pubkey.from_string(self.wallet.address)).value return v elif self.chain_type == NetworkTypes.LIGHTNING: lnpay_client = LNPayClient( @@ -331,9 +309,7 @@ def get_wallet_balance(self): return lnpay_client.get_balance() raise Exception("Invalid chain type") except Exception as e: - logging.exception( - f"Error getting wallet balance for {self.chain_name} error is {e}" - ) + logging.exception(f"Error getting wallet balance for {self.chain_name} error is {e}") return 0 @property @@ -352,7 +328,8 @@ def gas_price(self): from faucet.faucet_manager.fund_manager import EVMFundManager return EVMFundManager(self).w3.eth.gas_price - except: + except Exception as e: + logging.exception(f"Error getting gas price for {self.chain_name} error is {e}") return self.max_gas_price + 1 @property @@ -364,7 +341,8 @@ def is_gas_price_too_high(self): from faucet.faucet_manager.fund_manager import EVMFundManager return EVMFundManager(self).is_gas_price_too_high - except: + except Exception as e: + logging.exception(f"Error getting gas price for {self.chain_name} error is {e}") return True @property @@ -383,61 +361,35 @@ def total_claims(self): return total_claims @property - def total_claims_since_last_monday(self): - cached_total_claims_since_last_monday = cache.get( - f"gas_tap_chain_total_claims_since_last_monday_{self.pk}" - ) - if cached_total_claims_since_last_monday: - return cached_total_claims_since_last_monday - from faucet.faucet_manager.claim_manager import WeeklyCreditStrategy - - total_claims_since_last_monday = ClaimReceipt.objects.filter( - chain=self, - datetime__gte=WeeklyCreditStrategy.get_last_monday(), - _status__in=[ClaimReceipt.VERIFIED, BrightUser.VERIFIED], - ).count() - cache.set( - f"gas_tap_chain_total_claims_since_last_monday_{self.pk}", - total_claims_since_last_monday, - get_cache_time(self.pk), - ) - return total_claims_since_last_monday - - @property - def total_claims_for_last_round(self): - cached_total_claims_for_last_round = cache.get( - f"gas_tap_chain_total_claims_for_last_round_{self.pk}" - ) - if cached_total_claims_for_last_round: - return cached_total_claims_for_last_round - from faucet.faucet_manager.claim_manager import WeeklyCreditStrategy + def total_claims_this_round(self): + cached_total_claims_this_round = cache.get(f"gas_tap_chain_total_claims_this_round_{self.pk}") + if cached_total_claims_this_round: + return cached_total_claims_this_round + from faucet.faucet_manager.claim_manager import RoundCreditStrategy - total_claims_for_last_round = ClaimReceipt.objects.filter( + total_claims_this_round = ClaimReceipt.objects.filter( chain=self, - datetime__gte=WeeklyCreditStrategy.get_second_last_monday(), - datetime__lte=WeeklyCreditStrategy.get_last_monday(), - _status__in=[ClaimReceipt.VERIFIED, BrightUser.VERIFIED], + datetime__gte=RoundCreditStrategy.get_start_of_the_round(), + _status__in=[ClaimReceipt.VERIFIED], ).count() cache.set( - f"gas_tap_chain_total_claims_for_last_round_{self.pk}", - total_claims_for_last_round, + f"gas_tap_chain_total_claims_this_round_{self.pk}", + total_claims_this_round, get_cache_time(self.pk), ) - return total_claims_for_last_round + return total_claims_this_round @property def total_claims_since_last_round(self): - cached_total_claims_since_last_round = cache.get( - f"gas_tap_chain_total_claims_since_last_round_{self.pk}" - ) + cached_total_claims_since_last_round = cache.get(f"gas_tap_chain_total_claims_since_last_round_{self.pk}") if cached_total_claims_since_last_round: return cached_total_claims_since_last_round - from faucet.faucet_manager.claim_manager import WeeklyCreditStrategy + from faucet.faucet_manager.claim_manager import RoundCreditStrategy total_claims_since_last_round = ClaimReceipt.objects.filter( chain=self, - datetime__gte=WeeklyCreditStrategy.get_second_last_monday(), - _status__in=[ClaimReceipt.VERIFIED, BrightUser.VERIFIED], + datetime__gte=RoundCreditStrategy.get_start_of_previous_round(), + _status__in=[ClaimReceipt.VERIFIED], ).count() cache.set( f"gas_tap_chain_total_claims_since_last_round_{self.pk}", @@ -448,16 +400,14 @@ def total_claims_since_last_round(self): class GlobalSettings(models.Model): - weekly_chain_claim_limit = models.IntegerField(default=5) - tokentap_weekly_claim_limit = models.IntegerField(default=3) - prizetap_weekly_claim_limit = models.IntegerField(default=3) + gastap_round_claim_limit = models.IntegerField(default=5) + tokentap_round_claim_limit = models.IntegerField(default=3) + prizetap_round_claim_limit = models.IntegerField(default=3) is_gas_tap_available = models.BooleanField(default=True) class TransactionBatch(models.Model): - chain = models.ForeignKey( - Chain, related_name="batches", on_delete=models.PROTECT, db_index=True - ) + 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, db_index=True) diff --git a/faucet/serializers.py b/faucet/serializers.py index 1fb19b25..5c42a209 100644 --- a/faucet/serializers.py +++ b/faucet/serializers.py @@ -1,46 +1,43 @@ from rest_framework import serializers -from faucet.faucet_manager.claim_manager import LimitedChainClaimManager -from faucet.faucet_manager.credit_strategy import CreditStrategyFactory -from faucet.models import BrightUser, Chain, ClaimReceipt, GlobalSettings, DonationReceipt - - -class UserSerializer(serializers.ModelSerializer): - total_weekly_claims_remaining = serializers.SerializerMethodField() - - class Meta: - model = BrightUser - fields = [ - "pk", - "context_id", - "address", - "verification_url", - "verification_status", - "total_weekly_claims_remaining", - ] - read_only_fields = ["context_id"] - - 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) - ) - - def create(self, validated_data): - address = validated_data["address"] - bright_user = BrightUser.objects.get_or_create(address) - return bright_user +from faucet.models import Chain, ClaimReceipt, DonationReceipt, GlobalSettings + +# class UserSerializer(serializers.ModelSerializer): +# total_weekly_claims_remaining = serializers.SerializerMethodField() + +# class Meta: +# model = BrightUser +# fields = [ +# "pk", +# "context_id", +# "address", +# "verification_url", +# "verification_status", +# "total_weekly_claims_remaining", +# ] +# read_only_fields = ["context_id"] + +# 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) +# ) + +# def create(self, validated_data): +# address = validated_data["address"] +# bright_user = BrightUser.objects.get_or_create(address) +# return bright_user class GlobalSettingsSerializer(serializers.ModelSerializer): class Meta: model = GlobalSettings fields = [ - "weekly_chain_claim_limit", - "tokentap_weekly_claim_limit", - "prizetap_weekly_claim_limit", + "gastap_round_claim_limit", + "tokentap_round_claim_limit", + "prizetap_round_claim_limit", "is_gas_tap_available", ] @@ -104,8 +101,8 @@ class Meta: class ChainSerializer(serializers.ModelSerializer): - claimed = serializers.SerializerMethodField() - unclaimed = serializers.SerializerMethodField() + # claimed = serializers.SerializerMethodField() + # unclaimed = serializers.SerializerMethodField() class Meta: model = Chain @@ -123,11 +120,10 @@ class Meta: "modal_url", "gas_image_url", "max_claim_amount", - "claimed", - "unclaimed", - # "order", + # "claimed", + # "unclaimed", "total_claims", - "total_claims_since_last_monday", + "total_claims_this_round", "tokentap_contract_address", "needs_funding", "is_testnet", @@ -135,21 +131,21 @@ class Meta: "block_scan_address", ] - def get_claimed(self, chain) -> int: - user = self.context["request"].user + # def get_claimed(self, chain) -> int: + # user = self.context["request"].user - if not user.is_authenticated: - return "N/A" - user_profile = user.profile - return CreditStrategyFactory(chain, user_profile).get_strategy().get_claimed() + # if not user.is_authenticated: + # return "N/A" + # user_profile = user.profile + # return CreditStrategyFactory(chain, user_profile).get_strategy().get_claimed() - def get_unclaimed(self, chain) -> int: - user = self.context["request"].user + # def get_unclaimed(self, chain) -> int: + # user = self.context["request"].user - if not user.is_authenticated: - return "N/A" - user_profile = user.profile - return CreditStrategyFactory(chain, user_profile).get_strategy().get_unclaimed() + # if not user.is_authenticated: + # return "N/A" + # user_profile = user.profile + # return CreditStrategyFactory(chain, user_profile).get_strategy().get_unclaimed() class ReceiptSerializer(serializers.ModelSerializer): @@ -173,39 +169,23 @@ class DonationReceiptSerializer(serializers.ModelSerializer): chain = SmallChainSerializer(read_only=True) def validate(self, attrs): - chain = self._validate_chain(attrs.pop('chain_pk')) - attrs['user_profile'] = self.context.get('user') - attrs['chain'] = chain + chain = self._validate_chain(attrs.pop("chain_pk")) + attrs["user_profile"] = self.context.get("user") + attrs["chain"] = chain return attrs def _validate_chain(self, pk: str): try: - chain: Chain = Chain.objects.get(pk=pk, chain_type='EVM') + chain: Chain = Chain.objects.get(pk=pk, chain_type="EVM") except Chain.DoesNotExist: - raise serializers.ValidationError({'chain': 'chain is not EVM or does not exist.'}) + 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_pk", - "status", - "user_profile" - ] - read_only_fields = [ - 'value', - 'datetime', - 'total_price', - 'chain', - 'status', - "user_profile" - ] + fields = ["tx_hash", "chain", "datetime", "total_price", "value", "chain_pk", "status", "user_profile"] + read_only_fields = ["value", "datetime", "total_price", "chain", "status", "user_profile"] class LeaderboardSerializer(serializers.Serializer): diff --git a/faucet/tasks.py b/faucet/tasks.py index e5815d9f..f8515cf5 100644 --- a/faucet/tasks.py +++ b/faucet/tasks.py @@ -1,26 +1,29 @@ -import time -import logging import decimal +import logging +import time from contextlib import contextmanager -import web3.exceptions + import requests +import web3.exceptions from celery import shared_task +from django.conf import settings as django_settings from django.core.cache import cache from django.db import transaction from django.db.models import F, Func from django.utils import timezone from sentry_sdk import capture_exception + from authentication.models import NetworkTypes, Wallet +from core.models import TokenPrice from tokenTap.models import TokenDistributionClaim -from django.conf import settings as django_settings + from .faucet_manager.fund_manager import ( EVMFundManager, - SolanaFundManager, - LightningFundManager, FundMangerException, + LightningFundManager, + SolanaFundManager, ) -from core.models import TokenPrice -from .models import Chain, ClaimReceipt, TransactionBatch, DonationReceipt +from .models import Chain, ClaimReceipt, DonationReceipt, TransactionBatch @contextmanager @@ -42,9 +45,7 @@ def memcache_lock(lock_id, oid, lock_expire=60): def has_pending_batch(chain): - return TransactionBatch.objects.filter( - chain=chain, _status=ClaimReceipt.PENDING - ).exists() + return TransactionBatch.objects.filter(chain=chain, _status=ClaimReceipt.PENDING).exists() def passive_address_is_not_none(address): @@ -99,15 +100,10 @@ def process_batch(self, batch_pk): manager = SolanaFundManager(batch.chain) 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 - ): + elif batch.chain.chain_type == NetworkTypes.EVM or batch.chain.chain_type == NetworkTypes.NONEVMXDC: manager = EVMFundManager(batch.chain) else: - raise Exception( - f"Invalid chain type to process batch, chain type {batch.chain.chain_type}" - ) + raise Exception(f"Invalid chain type to process batch, chain type {batch.chain.chain_type}") tx_hash = manager.multi_transfer(data) batch.tx_hash = tx_hash batch.save() @@ -126,9 +122,7 @@ def process_batch(self, batch_pk): @shared_task def process_pending_batches(): - batches = TransactionBatch.objects.filter( - _status=ClaimReceipt.PENDING, tx_hash=None - ) + batches = TransactionBatch.objects.filter(_status=ClaimReceipt.PENDING, tx_hash=None) for _batch in batches: process_batch.delay(_batch.pk) @@ -153,15 +147,10 @@ def update_pending_batch_with_tx_hash(self, batch_pk): manager = SolanaFundManager(batch.chain) 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 - ): + elif batch.chain.chain_type == NetworkTypes.EVM or batch.chain.chain_type == NetworkTypes.NONEVMXDC: manager = EVMFundManager(batch.chain) else: - raise Exception( - f"Invalid chain type to update pending batch, chain type {batch.chain.chain_type}" - ) + raise Exception(f"Invalid chain type to update pending batch, chain type {batch.chain.chain_type}") if manager.is_tx_verified(batch.tx_hash): batch._status = ClaimReceipt.VERIFIED @@ -184,17 +173,14 @@ def reject_expired_pending_claims(): ClaimReceipt.objects.filter( batch=None, _status=ClaimReceipt.PENDING, - datetime__lte=timezone.now() - - timezone.timedelta(minutes=ClaimReceipt.MAX_PENDING_DURATION), + datetime__lte=timezone.now() - timezone.timedelta(minutes=ClaimReceipt.MAX_PENDING_DURATION), ).update(_status=ClaimReceipt.REJECTED) @shared_task def update_pending_batches_with_tx_hash_status(): batches_queryset = ( - TransactionBatch.objects.filter(_status=ClaimReceipt.PENDING) - .exclude(tx_hash=None) - .exclude(updating=True) + TransactionBatch.objects.filter(_status=ClaimReceipt.PENDING).exclude(tx_hash=None).exclude(updating=True) ) for _batch in batches_queryset: update_pending_batch_with_tx_hash.delay(_batch.pk) @@ -203,9 +189,7 @@ def update_pending_batches_with_tx_hash_status(): @shared_task def process_chain_pending_claims(chain_id): # locks chain with transaction.atomic(): - chain = Chain.objects.select_for_update().get( - pk=chain_id - ) # lock based on chain + chain = Chain.objects.select_for_update().get(pk=chain_id) # lock based on chain # all pending batches must be resolved before new transactions can be made if has_pending_batch(chain): @@ -213,9 +197,7 @@ def process_chain_pending_claims(chain_id): # locks chain # get all pending receipts for this chain # pending receipts are receipts that have not been batched yet - receipts = ClaimReceipt.objects.filter( - chain=chain, _status=ClaimReceipt.PENDING, batch=None - ) + receipts = ClaimReceipt.objects.filter(chain=chain, _status=ClaimReceipt.PENDING, batch=None) if receipts.count() == 0: return @@ -253,7 +235,7 @@ def update_needs_funding_status_chain(chain_id): chain.needs_funding = False chain.save() - except: + except Exception: capture_exception() @@ -279,7 +261,8 @@ def process_verified_lighning_claim(gas_tap_claim_id): ) if not tokentap_lightning_claim: - raise Exception("No tokentap claim found for user") + print("No tokentap claim found for user") + return tokentap_lightning_claim.status = ClaimReceipt.VERIFIED tokentap_lightning_claim.tx_hash = claim.tx_hash @@ -308,7 +291,8 @@ def process_rejected_lighning_claim(gas_tap_claim_id): ) if not tokentap_lightning_claim: - raise Exception("No tokentap claim found for user") + print("No tokentap claim found for user") + return tokentap_lightning_claim.delete() @@ -334,7 +318,6 @@ def update_tokentap_claim_for_verified_lightning_claims(): process_rejected_lighning_claim.apply((_claim.pk,)) else: if _claim._status == ClaimReceipt.VERIFIED: - process_verified_lighning_claim.delay( _claim.pk, ) @@ -351,35 +334,37 @@ def update_tokens_price(): """ # TODO: we can make this function performance better by using aiohttp and asyncio or Threads - tokens = TokenPrice.objects.exclude(price_url__isnull=True) + tokens = TokenPrice.objects.exclude(price_url__isnull=True).exclude(price_url="") 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'] + 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)}') + 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)}') + 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)}') + 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}') + 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: @@ -392,22 +377,26 @@ def process_donation_receipt(self, donation_receipt_pk): 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): + 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(): + 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 = str(evm_fund_manager.from_wei(tx.get('value'))) + donation_receipt.value = str(evm_fund_manager.from_wei(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(donation_receipt.value) * decimal.Decimal(token_price.usd_price)) + decimal.Decimal(donation_receipt.value) * decimal.Decimal(token_price.usd_price) + ) except TokenPrice.DoesNotExist: - logging.error(f'TokenPrice for Chain: {donation_receipt.chain.chain_name} did not defined') + logging.error(f"TokenPrice for Chain: {donation_receipt.chain.chain_name} did not defined") donation_receipt.status = ClaimReceipt.PROCESSED_FOR_TOKENTAP_REJECT donation_receipt.save() return diff --git a/faucet/test.py b/faucet/test.py index e8321f01..d7a651ef 100644 --- a/faucet/test.py +++ b/faucet/test.py @@ -9,14 +9,14 @@ from django.utils import timezone from rest_framework.test import APITestCase -from authentication.models import UserProfile +from authentication.models import UserProfile, Wallet from brightIDfaucet.settings import DEBUG from faucet.constants import MEMCACHE_LIGHTNING_LOCK_KEY from faucet.constraints import OptimismClaimingGasConstraint, OptimismDonationConstraint from faucet.faucet_manager.claim_manager import ClaimManagerFactory, SimpleClaimManager from faucet.faucet_manager.credit_strategy import ( + RoundCreditStrategy, SimpleCreditStrategy, - WeeklyCreditStrategy, ) from faucet.faucet_manager.fund_manager import LightningFundManager from faucet.helpers import memcache_lock @@ -28,7 +28,6 @@ LightningConfig, NetworkTypes, TransactionBatch, - Wallet, WalletAccount, ) from faucet.views import CustomException @@ -198,36 +197,36 @@ def test_list_chains(self): response = self.request_chain_list() self.assertEqual(response.status_code, 200) - def test_list_chain_should_show_NA_if_no_addresses_provided(self): - chains = self.request_chain_list() - chains_list = json.loads(chains.content) + # def test_list_chain_should_show_NA_if_no_addresses_provided(self): + # chains = self.request_chain_list() + # chains_list = json.loads(chains.content) - for chain_data in chains_list: - self.assertEqual(chain_data["claimed"], "N/A") - self.assertEqual(chain_data["unclaimed"], "N/A") - if chain_data["symbol"] == "XDAI": - self.assertEqual(chain_data["maxClaimAmount"], x_dai_max_claim) - elif chain_data["symbol"] == "eidi": - self.assertEqual(chain_data["maxClaimAmount"], eidi_max_claim) + # for chain_data in chains_list: + # self.assertEqual(chain_data["claimed"], "N/A") + # self.assertEqual(chain_data["unclaimed"], "N/A") + # if chain_data["symbol"] == "XDAI": + # self.assertEqual(chain_data["maxClaimAmount"], x_dai_max_claim) + # elif chain_data["symbol"] == "eidi": + # self.assertEqual(chain_data["maxClaimAmount"], eidi_max_claim) - def test_chain_list_without_token(self): - endpoint = reverse("FAUCET:chain-list") - chain_list_response = self.client.get(endpoint) - chain_list = json.loads(chain_list_response.content) + # def test_chain_list_without_token(self): + # endpoint = reverse("FAUCET:chain-list") + # chain_list_response = self.client.get(endpoint) + # chain_list = json.loads(chain_list_response.content) - for chain_data in chain_list: - self.assertEqual(chain_data["claimed"], "N/A") - self.assertEqual(chain_data["unclaimed"], "N/A") + # for chain_data in chain_list: + # self.assertEqual(chain_data["claimed"], "N/A") + # self.assertEqual(chain_data["unclaimed"], "N/A") - def test_chain_list_with_token(self): - endpoint = reverse("FAUCET:chain-list") - self.client.force_authenticate(user=self.new_user.user) - chain_list_response = self.client.get(endpoint) - chain_list = json.loads(chain_list_response.content) + # def test_chain_list_with_token(self): + # endpoint = reverse("FAUCET:chain-list") + # self.client.force_authenticate(user=self.new_user.user) + # chain_list_response = self.client.get(endpoint) + # chain_list = json.loads(chain_list_response.content) - for chain_data in chain_list: - self.assertEqual(chain_data["claimed"], 0) - self.assertEqual(chain_data["unclaimed"], chain_data["maxClaimAmount"]) + # for chain_data in chain_list: + # self.assertEqual(chain_data["claimed"], 0) + # self.assertEqual(chain_data["unclaimed"], chain_data["maxClaimAmount"]) class TestClaim(APITestCase): @@ -238,11 +237,11 @@ def setUp(self) -> None: self.x_dai = create_xDai_chain(self.wallet) self.idChain = create_idChain_chain(self.wallet) self.test_chain = create_test_chain(self.wallet) - GlobalSettings.objects.create(weekly_chain_claim_limit=2) + GlobalSettings.objects.create(gastap_round_claim_limit=2) def test_get_claimed_should_be_zero(self): - credit_strategy_xdai = WeeklyCreditStrategy(self.x_dai, self.new_user) - credit_strategy_id_chain = WeeklyCreditStrategy(self.idChain, self.new_user) + credit_strategy_xdai = RoundCreditStrategy(self.x_dai, self.new_user) + credit_strategy_id_chain = RoundCreditStrategy(self.idChain, self.new_user) self.assertEqual(credit_strategy_xdai.get_claimed(), 0) self.assertEqual(credit_strategy_id_chain.get_claimed(), 0) @@ -259,8 +258,8 @@ def test_x_dai_claimed_be_zero_eth_be_100(self): amount=claim_amount, ) - credit_strategy_xdai = WeeklyCreditStrategy(self.x_dai, self.new_user) - credit_strategy_id_chain = WeeklyCreditStrategy(self.idChain, self.new_user) + credit_strategy_xdai = RoundCreditStrategy(self.x_dai, self.new_user) + credit_strategy_id_chain = RoundCreditStrategy(self.idChain, self.new_user) self.assertEqual(credit_strategy_xdai.get_claimed(), 0) self.assertEqual(credit_strategy_id_chain.get_claimed(), claim_amount) @@ -268,7 +267,7 @@ def test_x_dai_claimed_be_zero_eth_be_100(self): self.assertEqual(credit_strategy_id_chain.get_unclaimed(), eidi_max_claim - claim_amount) def test_claim_manager_fail_if_claim_amount_exceeds_unclaimed(self): - claim_manager_x_dai = SimpleClaimManager(WeeklyCreditStrategy(self.x_dai, self.new_user)) + claim_manager_x_dai = SimpleClaimManager(RoundCreditStrategy(self.x_dai, self.new_user)) try: claim_manager_x_dai.claim(x_dai_max_claim + 10) @@ -282,7 +281,7 @@ def test_claim_manager_fail_if_claim_amount_exceeds_unclaimed(self): ) def test_claim_unverified_user_should_fail(self): claim_amount = 100 - claim_manager_x_dai = SimpleClaimManager(WeeklyCreditStrategy(self.x_dai, self.new_user)) + claim_manager_x_dai = SimpleClaimManager(RoundCreditStrategy(self.x_dai, self.new_user)) try: claim_manager_x_dai.claim(claim_amount) @@ -396,7 +395,7 @@ def setUp(self) -> None: self.password = "test" self._address = "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9" - GlobalSettings.objects.create(weekly_chain_claim_limit=2) + GlobalSettings.objects.create(gastap_round_claim_limit=2) LightningConfig.objects.create( period=86800, period_max_cap=100, current_round=int(int(time.time()) / 86800) * 86800 ) @@ -566,15 +565,15 @@ def setUp(self) -> None: self.password = "test" self._address = "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9" - GlobalSettings.objects.create(weekly_chain_claim_limit=2) + GlobalSettings.objects.create(gastap_round_claim_limit=2) self.user_profile = create_new_user(self._address) self.client.force_authenticate(user=self.user_profile.user) - self.strategy = WeeklyCreditStrategy(self.test_chain, self.user_profile) + self.strategy = RoundCreditStrategy(self.test_chain, self.user_profile) def test_last_monday(self): now = timezone.now() - last_monday = WeeklyCreditStrategy.get_last_monday() + last_monday = RoundCreditStrategy.get_start_of_the_round() self.assertGreaterEqual(now, last_monday) def create_claim_receipt(self, date, amount=10): @@ -591,7 +590,7 @@ def create_claim_receipt(self, date, amount=10): ) def test_last_week_claims(self): - last_monday = WeeklyCreditStrategy.get_last_monday() + last_monday = RoundCreditStrategy.get_start_of_the_round() last_sunday = last_monday - datetime.timedelta(days=1) tuesday = last_monday + datetime.timedelta(days=1) wednesday = last_monday + datetime.timedelta(days=2) @@ -606,7 +605,7 @@ def test_last_week_claims(self): self.assertEqual(total_claimed, 30) def test_unclaimed(self): - last_monday = WeeklyCreditStrategy.get_last_monday() + last_monday = RoundCreditStrategy.get_start_of_the_round() last_sunday = last_monday - datetime.timedelta(days=1) tuesday = last_monday + datetime.timedelta(days=1) diff --git a/faucet/urls.py b/faucet/urls.py index d4d2449e..ee488a5d 100644 --- a/faucet/urls.py +++ b/faucet/urls.py @@ -1,25 +1,24 @@ from django.urls import path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view from faucet.views import ( + ChainBalanceView, ChainListView, ClaimCountView, ClaimMaxView, + DonationReceiptView, + GetTotalRoundClaimsRemainingView, GlobalSettingsView, LastClaimView, + LeaderboardView, ListClaims, - GetTotalWeeklyClaimsRemainingView, + SmallChainListView, + UserLeaderboardView, artwork_video, error500, - ChainBalanceView, - SmallChainListView, - DonationReceiptView, - LeaderboardView, - UserLeaderboardView ) -from drf_yasg.views import get_schema_view -from drf_yasg import openapi - schema_view = get_schema_view( openapi.Info( title="BrightID Gas Faucet API", @@ -36,15 +35,13 @@ urlpatterns = [ path( "user/remainig-claims/", - GetTotalWeeklyClaimsRemainingView.as_view(), + GetTotalRoundClaimsRemainingView.as_view(), name="remaining-claims", ), 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 + path("chain/list/", ChainListView.as_view(), name="chain-list"), # can have auth token for more user specific info path("chain/small-list/", SmallChainListView.as_view(), name="small-chain-list"), path( "chain//claim-max/", @@ -67,5 +64,5 @@ ), path("user/donation/", DonationReceiptView.as_view(), name="donation-receipt"), path("gas-tap/leaderboard/", LeaderboardView.as_view(), name="gas-tap-leaderboard"), - path("user/gas-tap/leaderboard/", UserLeaderboardView.as_view(), name="user-gas-tap-leaderboard") + path("user/gas-tap/leaderboard/", UserLeaderboardView.as_view(), name="user-gas-tap-leaderboard"), ] diff --git a/faucet/views.py b/faucet/views.py index 8548dc7a..8d7b828d 100644 --- a/faucet/views.py +++ b/faucet/views.py @@ -1,41 +1,42 @@ import json -from django.http import FileResponse +import logging import os + import rest_framework.exceptions -from django.http import Http404 +from django.conf import settings +from django.contrib.postgres.expressions import ArraySubquery +from django.db.models import FloatField, OuterRef, Subquery, Sum +from django.db.models.functions import Cast +from django.http import FileResponse, Http404, HttpResponse +from django.urls import reverse from rest_framework.generics import ( - RetrieveAPIView, ListAPIView, - ListCreateAPIView, get_object_or_404, + ListCreateAPIView, + RetrieveAPIView, + get_object_or_404, ) +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from django.http import HttpResponse -from rest_framework.permissions import IsAuthenticated -from django.urls import reverse from authentication.models import UserProfile, Wallet +from core.filters import ChainFilterBackend, IsOwnerFilterBackend +from core.paginations import StandardResultsSetPagination from faucet.faucet_manager.claim_manager import ( ClaimManagerFactory, LimitedChainClaimManager, ) -from faucet.faucet_manager.claim_manager import WeeklyCreditStrategy -from faucet.models import Chain, ClaimReceipt, GlobalSettings, DonationReceipt +from faucet.faucet_manager.credit_strategy import RoundCreditStrategy +from faucet.models import Chain, ClaimReceipt, DonationReceipt, GlobalSettings from faucet.serializers import ( ChainBalanceSerializer, + ChainSerializer, + DonationReceiptSerializer, GlobalSettingsSerializer, + LeaderboardSerializer, ReceiptSerializer, - ChainSerializer, SmallChainSerializer, - DonationReceiptSerializer, LeaderboardSerializer, ) -from core.paginations import StandardResultsSetPagination -from core.filters import ChainFilterBackend, IsOwnerFilterBackend -# import BASE_DIR from django settings -from django.conf import settings -from django.db.models import FloatField, Sum, OuterRef, Subquery, Window, F, Count -from django.db.models.functions import Cast -from django.contrib.postgres.expressions import ArraySubquery class CustomException(Exception): @@ -55,11 +56,7 @@ class LastClaimView(RetrieveAPIView): def get_object(self): user_profile = self.request.user.profile try: - return ( - ClaimReceipt.objects.filter(user_profile=user_profile) - .order_by("pk") - .last() - ) + return ClaimReceipt.objects.filter(user_profile=user_profile).order_by("pk").last() except ClaimReceipt.DoesNotExist: raise Http404("Claim Receipt for this user does not exist") @@ -83,11 +80,11 @@ def get_queryset(self): ClaimReceipt.PENDING, ClaimReceipt.REJECTED, ], - datetime__gte=WeeklyCreditStrategy.get_last_monday(), + datetime__gte=RoundCreditStrategy.get_start_of_the_round(), ).order_by("-pk") -class GetTotalWeeklyClaimsRemainingView(RetrieveAPIView): +class GetTotalRoundClaimsRemainingView(RetrieveAPIView): """ Return the total weekly claims remaining for the given user """ @@ -98,11 +95,8 @@ def get(self, request, *args, **kwargs): user_profile = request.user.profile gs = GlobalSettings.objects.first() if gs is not None: - result = ( - gs.weekly_chain_claim_limit - - LimitedChainClaimManager.get_total_weekly_claims(user_profile) - ) - return Response({"total_weekly_claims_remaining": result}, status=200) + result = gs.gastap_round_claim_limit - LimitedChainClaimManager.get_total_round_claims(user_profile) + return Response({"total_round_claims_remaining": result}, status=200) else: raise Http404("Global Settings Not Found") @@ -119,9 +113,7 @@ class ChainListView(ListAPIView): def get_queryset(self): queryset = Chain.objects.filter(is_active=True, show_in_gastap=True) - sorted_queryset = sorted( - queryset, key=lambda obj: obj.total_claims_since_last_round, reverse=True - ) + sorted_queryset = sorted(queryset, key=lambda obj: obj.total_claims_since_last_round, reverse=True) return sorted_queryset @@ -169,13 +161,11 @@ def wallet_address_is_set(self): chain = self.get_chain() try: - _wallet = Wallet.objects.get( - user_profile=self.get_user(), wallet_type=chain.chain_type - ) + Wallet.objects.get(user_profile=self.get_user(), wallet_type=chain.chain_type) return True, None except Exception as e: + logging.error("wallet address not set", e) raise CustomException("wallet address not set") - # return Response({"message": "wallet address not set"}, status=403) def get_chain(self) -> Chain: chain_pk = self.kwargs.get("chain_pk", None) @@ -194,7 +184,7 @@ def claim_max(self, passive_address) -> ClaimReceipt: assert max_credit > 0 return manager.claim(max_credit, passive_address=passive_address) except AssertionError as e: - # return Response({"message": "no credit left"}, status=403) + logging.error("no credit left for user", e) raise CustomException("no credit left") except ValueError as e: raise rest_framework.exceptions.APIException(e) @@ -228,7 +218,7 @@ class DonationReceiptView(ListCreateAPIView): def get_serializer_context(self): context = super().get_serializer_context() - context.update({'user': self.get_user()}) + context.update({"user": self.get_user()}) return context def get_queryset(self): @@ -249,20 +239,24 @@ def get_user(self) -> UserProfile: def get_object(self): queryset = self.filter_queryset(self.get_queryset()) - queryset = queryset.filter(status=ClaimReceipt.VERIFIED) \ - .annotate( - total_price_float=Cast('total_price', FloatField())).values('user_profile') \ - .annotate( - sum_total_price=Sum('total_price_float')) + queryset = ( + queryset.filter(status=ClaimReceipt.VERIFIED) + .annotate(total_price_float=Cast("total_price", FloatField())) + .values("user_profile") + .annotate(sum_total_price=Sum("total_price_float")) + ) user_obj = get_object_or_404(queryset, user_profile=self.get_user().pk) - 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 - interacted_chains = list(DonationReceipt.objects.filter( - user_profile=self.get_user()).filter(status=ClaimReceipt.VERIFIED).values_list( - 'chain', flat=True).distinct()) - user_obj['interacted_chains'] = interacted_chains + 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 + interacted_chains = list( + DonationReceipt.objects.filter(user_profile=self.get_user()) + .filter(status=ClaimReceipt.VERIFIED) + .values_list("chain", flat=True) + .distinct() + ) + user_obj["interacted_chains"] = interacted_chains return user_obj @@ -275,15 +269,22 @@ class LeaderboardView(ListAPIView): def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) - donation_receipt = queryset.filter(status=ClaimReceipt.VERIFIED).annotate( - total_price_float=Cast('total_price', FloatField())).values('user_profile').annotate( - sum_total_price=Sum('total_price_float')).order_by('-sum_total_price') - subquery_interacted_chains = DonationReceipt.objects.filter( - user_profile=OuterRef('user_profile')).filter(status=ClaimReceipt.VERIFIED).values_list( - 'chain', flat=True).distinct() + donation_receipt = ( + queryset.filter(status=ClaimReceipt.VERIFIED) + .annotate(total_price_float=Cast("total_price", FloatField())) + .values("user_profile") + .annotate(sum_total_price=Sum("total_price_float")) + .order_by("-sum_total_price") + ) + subquery_interacted_chains = ( + DonationReceipt.objects.filter(user_profile=OuterRef("user_profile")) + .filter(status=ClaimReceipt.VERIFIED) + .values_list("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_username = UserProfile.objects.filter(pk=OuterRef("user_profile")).values("username") + subquery_wallet = Wallet.objects.filter(user_profile=OuterRef("user_profile")).values("address") queryset = queryset.annotate(username=Subquery(subquery_username), wallet=Subquery(subquery_wallet)) page = self.paginate_queryset(queryset) if page is not None: @@ -295,7 +296,7 @@ def list(self, request, *args, **kwargs): def artwork_video(request): - video_file = os.path.join(settings.BASE_DIR, f"faucet/artwork.mp4") + video_file = os.path.join(settings.BASE_DIR, "faucet/artwork.mp4") return FileResponse(open(video_file, "rb"), content_type="video/mp4") @@ -305,7 +306,9 @@ def artwork_view(request, token_id): response = { "name": "Unitap Pass", - "description": "Unitap is an onboarding tool for networks and communities and a gateway for users to web3. https://unitap.app . Unitap Pass is a VIP pass for Unitap. Holders will enjoy various benefits as Unitap grows.", + "description": "Unitap is an onboarding tool for networks and communities and a gateway for users to web3." + " https://unitap.app . Unitap Pass is a VIP pass for Unitap. Holders" + " will enjoy various benefits as Unitap grows.", "image": artwork_video_url, "animation_url": artwork_video_url, } diff --git a/permissions/admin.py b/permissions/admin.py index 7933cfe0..018dd20c 100644 --- a/permissions/admin.py +++ b/permissions/admin.py @@ -1,5 +1,12 @@ from django.contrib import admin -from .models import * + +from .models import ( + BrightIDAuraVerification, + BrightIDMeetVerification, + OnceInALifeTimeVerification, + OncePerMonthVerification, + OncePerWeekVerification, +) # Register your models here. diff --git a/permissions/models.py b/permissions/models.py index 61ffc1fb..c1d5e0a7 100644 --- a/permissions/models.py +++ b/permissions/models.py @@ -1,11 +1,10 @@ from django.db import models from polymorphic.models import PolymorphicModel -from faucet.faucet_manager.credit_strategy import WeeklyCreditStrategy +from faucet.faucet_manager.credit_strategy import RoundCreditStrategy class Permission(PolymorphicModel): - name = models.CharField(max_length=200) description = models.TextField(null=True, blank=True) @@ -38,7 +37,7 @@ def is_valid(self, user_profile, *args, **kwargs): token_distribution = kwargs.get("token_distribution") return not token_distribution.claims.filter( user_profile=user_profile, - created_at__gte=WeeklyCreditStrategy.get_last_monday(), + created_at__gte=RoundCreditStrategy.get_start_of_the_round(), ).exists() def response(self): @@ -50,7 +49,7 @@ def is_valid(self, user_profile, *args, **kwargs): token_distribution = kwargs.get("token_distribution") return not token_distribution.claims.filter( user_profile=user_profile, - created_at__gte=WeeklyCreditStrategy.get_first_day_of_the_month(), + created_at__gte=RoundCreditStrategy.get_start_of_the_round(), ).exists() def response(self): diff --git a/permissions/serializers.py b/permissions/serializers.py index e815efb4..42d21590 100644 --- a/permissions/serializers.py +++ b/permissions/serializers.py @@ -1,11 +1,10 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer - from permissions.models import ( - Permission, - BrightIDMeetVerification, BrightIDAuraVerification, + BrightIDMeetVerification, + Permission, ) diff --git a/permissions/tests.py b/permissions/tests.py index 23086d7d..3487ab0b 100644 --- a/permissions/tests.py +++ b/permissions/tests.py @@ -1,29 +1,28 @@ -from rest_framework.test import APITestCase -from .models import * +# from rest_framework.test import APITestCase +# from .models import * -class PermissionsTestCase(APITestCase): - def setUp(self) -> None: - pass +# class PermissionsTestCase(APITestCase): +# def setUp(self) -> None: +# pass - def test_permissions_creation(self): - p = BrightIDMeetVerification.objects.create(name="BrightID Meet Verification") - self.assertEqual(Permission.objects.count(), 1) - self.assertEqual(Permission.objects.first(), p) +# def test_permissions_creation(self): +# p = BrightIDMeetVerification.objects.create(name="BrightID Meet Verification") +# self.assertEqual(Permission.objects.count(), 1) +# self.assertEqual(Permission.objects.first(), p) - p2 = BrightIDAuraVerification.objects.create(name="BrightID Aura Verification") - self.assertEqual(Permission.objects.count(), 2) - self.assertEqual(Permission.objects.last(), p2) +# p2 = BrightIDAuraVerification.objects.create(name="BrightID Aura Verification") +# self.assertEqual(Permission.objects.count(), 2) +# self.assertEqual(Permission.objects.last(), p2) - p3 = OncePerWeekVerification.objects.create(name="Once Per Week Verification") - self.assertEqual(Permission.objects.count(), 3) - self.assertEqual(Permission.objects.last(), p3) +# p3 = OncePerWeekVerification.objects.create(name="Once Per Week Verification") +# self.assertEqual(Permission.objects.count(), 3) +# self.assertEqual(Permission.objects.last(), p3) - p4 = OncePerMonthVerification.objects.create(name="Once Per Month Verification") - self.assertEqual(Permission.objects.count(), 4) - self.assertEqual(Permission.objects.last(), p4) +# p4 = OncePerMonthVerification.objects.create(name="Once Per Month Verification") +# self.assertEqual(Permission.objects.count(), 4) +# self.assertEqual(Permission.objects.last(), p4) - p5 = OnceInALifeTimeVerification.objects.create(name="Once In A Life Time Verification") - self.assertEqual(Permission.objects.count(), 5) - self.assertEqual(Permission.objects.last(), p5) - +# p5 = OnceInALifeTimeVerification.objects.create(name="Once In A Life Time Verification") +# self.assertEqual(Permission.objects.count(), 5) +# self.assertEqual(Permission.objects.last(), p5) diff --git a/tokenTap/constraints.py b/tokenTap/constraints.py index 5e76522f..626ab7c1 100644 --- a/tokenTap/constraints.py +++ b/tokenTap/constraints.py @@ -1,16 +1,14 @@ -from core.constraints import * +from core.constraints import ConstraintVerification from core.utils import TimeUtils -from faucet.constraints import OptimismHasClaimedGasInThisRound from faucet.models import ClaimReceipt - -class OncePerWeekVerification(ConstraintVerification): - def is_observed(self, *args, **kwargs): - token_distribution = kwargs["token_distribution"] - return not token_distribution.claims.filter( - user_profile=self.user_profile, - created_at__gte=TimeUtils.get_last_monday(), - ).exists() +# class OncePerWeekVerification(ConstraintVerification): +# def is_observed(self, *args, **kwargs): +# token_distribution = kwargs["token_distribution"] +# return not token_distribution.claims.filter( +# user_profile=self.user_profile, +# created_at__gte=TimeUtils.get_first_day_of_the_month(), +# ).exists() class OncePerMonthVerification(ConstraintVerification): diff --git a/tokenTap/helpers.py b/tokenTap/helpers.py index b7d0f254..eb6fe373 100644 --- a/tokenTap/helpers.py +++ b/tokenTap/helpers.py @@ -1,11 +1,13 @@ import random -from web3 import Web3, Account -from faucet.faucet_manager.credit_strategy import WeeklyCreditStrategy -from faucet.models import GlobalSettings -from .models import TokenDistributionClaim -from authentication.models import NetworkTypes -from faucet.models import WalletAccount + from eth_account.messages import encode_defunct +from web3 import Account, Web3 + +from authentication.models import NetworkTypes +from faucet.faucet_manager.credit_strategy import RoundCreditStrategy +from faucet.models import GlobalSettings, WalletAccount + +from .models import TokenDistributionClaim def create_uint32_random_nonce(): @@ -19,7 +21,6 @@ def create_uint32_random_nonce(): def hash_message(user, token, amount, nonce): - message_hash = Web3().solidity_keccak( ["address", "address", "uint256", "uint32"], [Web3.to_checksum_address(user), Web3.to_checksum_address(token), amount, nonce], @@ -30,7 +31,6 @@ def hash_message(user, token, amount, nonce): def sign_hashed_message(hashed_message): - private_key = WalletAccount.objects.get(network_type=NetworkTypes.EVM).private_key account = Account.from_key(private_key) @@ -43,7 +43,7 @@ def has_weekly_credit_left(user_profile): return ( TokenDistributionClaim.objects.filter( user_profile=user_profile, - created_at__gte=WeeklyCreditStrategy.get_last_monday(), + created_at__gte=RoundCreditStrategy.get_start_of_the_round(), ).count() - < GlobalSettings.objects.first().tokentap_weekly_claim_limit + < GlobalSettings.objects.first().tokentap_round_claim_limit ) diff --git a/tokenTap/migrations/0019_alter_constraint_name.py b/tokenTap/migrations/0019_alter_constraint_name.py new file mode 100644 index 00000000..6092aee2 --- /dev/null +++ b/tokenTap/migrations/0019_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-10-24 13:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0018_constraint_icon_url'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('BrightIDMeetVerification', 'BrightIDMeetVerification'), ('BrightIDAuraVerification', 'BrightIDAuraVerification'), ('OncePerWeekVerification', 'OncePerWeekVerification'), ('OncePerMonthVerification', 'OncePerMonthVerification'), ('OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('OptimismHasClaimedGasInThisRound', 'OptimismHasClaimedGasInThisRound')], max_length=255, unique=True), + ), + ] diff --git a/tokenTap/migrations/0020_alter_constraint_name.py b/tokenTap/migrations/0020_alter_constraint_name.py new file mode 100644 index 00000000..7093dbb3 --- /dev/null +++ b/tokenTap/migrations/0020_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-10-27 18:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0019_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('BrightIDMeetVerification', 'BrightIDMeetVerification'), ('BrightIDAuraVerification', 'BrightIDAuraVerification'), ('OncePerMonthVerification', 'OncePerMonthVerification'), ('OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('OptimismHasClaimedGasInThisRound', 'OptimismHasClaimedGasInThisRound')], max_length=255, unique=True), + ), + ] diff --git a/tokenTap/models.py b/tokenTap/models.py index 57b3dbd5..310fedf6 100644 --- a/tokenTap/models.py +++ b/tokenTap/models.py @@ -1,16 +1,21 @@ -from django.utils import timezone +from django.core.cache import cache from django.db import models +from django.utils import timezone + from authentication.models import NetworkTypes, UserProfile -from faucet.models import Chain, ClaimReceipt -from faucet.constraints import OptimismHasClaimedGasInThisRound from core.models import UserConstraint -from .constraints import * -from django.core.cache import cache +from faucet.constraints import OptimismHasClaimedGasInThisRound +from faucet.models import Chain, ClaimReceipt + +from .constraints import ( + OnceInALifeTimeVerification, + OncePerMonthVerification, + TimeUtils, +) class Constraint(UserConstraint): constraints = UserConstraint.constraints + [ - OncePerWeekVerification, OncePerMonthVerification, OnceInALifeTimeVerification, OptimismHasClaimedGasInThisRound, @@ -31,9 +36,7 @@ class TokenDistribution(models.Model): token = models.CharField(max_length=100) token_address = models.CharField(max_length=255) amount = models.BigIntegerField() - chain = models.ForeignKey( - Chain, on_delete=models.CASCADE, related_name="token_distribution" - ) + chain = models.ForeignKey(Chain, on_delete=models.CASCADE, related_name="token_distribution") permissions = models.ManyToManyField(Constraint, blank=True) @@ -73,11 +76,10 @@ def total_claims_since_last_round(self): ) if cached_total_claims_since_last_round: return cached_total_claims_since_last_round - from faucet.faucet_manager.claim_manager import WeeklyCreditStrategy total_claims_since_last_round = TokenDistributionClaim.objects.filter( token_distribution=self, - created_at__gte=TimeUtils.get_second_last_monday(), + created_at__gte=TimeUtils.get_first_day_of_last_month(), status__in=[ClaimReceipt.VERIFIED, ClaimReceipt.PENDING], ).count() cache.set( @@ -92,12 +94,8 @@ def __str__(self): class TokenDistributionClaim(models.Model): - token_distribution = models.ForeignKey( - TokenDistribution, on_delete=models.CASCADE, related_name="claims" - ) - user_profile = models.ForeignKey( - UserProfile, on_delete=models.CASCADE, related_name="tokentap_claims" - ) + token_distribution = models.ForeignKey(TokenDistribution, on_delete=models.CASCADE, related_name="claims") + user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name="tokentap_claims") created_at = models.DateTimeField(auto_now_add=True, editable=True) notes = models.TextField(null=True, blank=True) @@ -105,9 +103,7 @@ class TokenDistributionClaim(models.Model): signature = models.CharField(max_length=1024, blank=True, null=True) nonce = models.BigIntegerField(null=True, blank=True) - status = models.CharField( - max_length=30, choices=ClaimReceipt.states, default=ClaimReceipt.PENDING - ) + status = models.CharField(max_length=30, choices=ClaimReceipt.states, default=ClaimReceipt.PENDING) tx_hash = models.CharField(max_length=255, null=True, blank=True) diff --git a/tokenTap/serializers.py b/tokenTap/serializers.py index e772d17f..06ae3fe8 100644 --- a/tokenTap/serializers.py +++ b/tokenTap/serializers.py @@ -1,8 +1,23 @@ -from faucet.serializers import SmallChainSerializer from rest_framework import serializers + +from core.constraints import ( # noqa: F401 + BrightIDAuraVerification, + BrightIDMeetVerification, +) from core.serializers import UserConstraintBaseSerializer -from tokenTap.models import * -from .constraints import * +from faucet.serializers import SmallChainSerializer +from tokenTap.models import ( + Constraint, + TokenDistribution, + TokenDistributionClaim, + UserConstraint, +) + +from .constraints import ( # noqa: F401 + ConstraintVerification, + OnceInALifeTimeVerification, + OncePerMonthVerification, +) class ConstraintSerializer(UserConstraintBaseSerializer, serializers.ModelSerializer): diff --git a/tokenTap/tests.py b/tokenTap/tests.py index 23265e63..0b5bd35a 100644 --- a/tokenTap/tests.py +++ b/tokenTap/tests.py @@ -4,18 +4,9 @@ from django.contrib.auth.models import User from django.urls import reverse from django.utils import timezone - -# from permissions.models import ( -# BrightIDAuraVerification, -# BrightIDMeetVerification, -# OncePerWeekVerification, -# OncePerMonthVerification, -# OnceInALifeTimeVerification, -# ) from rest_framework.test import APITestCase, override_settings from authentication.models import NetworkTypes, UserProfile, Wallet -from faucet.faucet_manager.credit_strategy import WeeklyCreditStrategy from faucet.models import ( Chain, ClaimReceipt, @@ -217,7 +208,6 @@ def setUp(self) -> None: ) self.permission1 = Constraint.objects.create(name="BrightIDMeetVerification", title="BrightID Meet", type="VER") self.permission2 = Constraint.objects.create(name="BrightIDAuraVerification", title="BrightID Aura", type="VER") - self.permission3 = Constraint.objects.create(name="OncePerWeekVerification", title="Once per Week", type="TIME") self.permission4 = Constraint.objects.create( name="OncePerMonthVerification", title="Once per Month", type="TIME" ) @@ -225,7 +215,7 @@ def setUp(self) -> None: name="OnceInALifeTimeVerification", title="Once per Lifetime", type="TIME" ) - self.td.permissions.set([self.permission1, self.permission2, self.permission3, self.permission4]) + self.td.permissions.set([self.permission1, self.permission2, self.permission4]) self.btc_td = TokenDistribution.objects.create( name="Test Distribution", @@ -322,29 +312,29 @@ def test_token_distribution_not_claimable_already_claimed(self): # response.data["detail"], "You have already claimed this token this week" # ) - @patch( - "authentication.helpers.BrightIDSoulboundAPIInterface.get_verification_status", - lambda a, b, c: (True, None), - ) - def test_token_distribution_not_claimable_already_claimed_month(self): - tdc = TokenDistributionClaim.objects.create( - user_profile=self.user_profile, - token_distribution=self.td, - # Claimed 2 weeks ago - created_at=WeeklyCreditStrategy.get_first_day_of_the_month(), - ) - tdc.created_at = WeeklyCreditStrategy.get_first_day_of_the_month() - tdc.save() - - self.client.force_authenticate(user=self.user_profile.user) - response = self.client.post( - reverse("token-distribution-claim", kwargs={"pk": self.td.pk}), - ) - - self.assertEqual(response.status_code, 403) - # self.assertEqual( - # response.data["detail"], "You have already claimed this token this month" - # ) + # @patch( + # "authentication.helpers.BrightIDSoulboundAPIInterface.get_verification_status", + # lambda a, b, c: (True, None), + # ) + # def test_token_distribution_not_claimable_already_claimed_month(self): + # tdc = TokenDistributionClaim.objects.create( + # user_profile=self.user_profile, + # token_distribution=self.td, + # # Claimed 2 weeks ago + # created_at=WeeklyCreditStrategy.get_first_day_of_the_month(), + # ) + # tdc.created_at = WeeklyCreditStrategy.get_first_day_of_the_month() + # tdc.save() + + # self.client.force_authenticate(user=self.user_profile.user) + # response = self.client.post( + # reverse("token-distribution-claim", kwargs={"pk": self.td.pk}), + # ) + + # self.assertEqual(response.status_code, 403) + # # self.assertEqual( + # # response.data["detail"], "You have already claimed this token this month" + # # ) @patch( "authentication.helpers.BrightIDSoulboundAPIInterface.get_verification_status", @@ -357,7 +347,7 @@ def test_token_distribution_not_claimable_false_permissions(self): self.assertEqual(response.status_code, 403) def test_token_distribution_not_claimable_weekly_credit_limit_reached(self): - self.global_settings.tokentap_weekly_claim_limit = 0 + self.global_settings.tokentap_round_claim_limit = 0 self.global_settings.save() self.client.force_authenticate(user=self.user_profile.user) diff --git a/tokenTap/views.py b/tokenTap/views.py index 0d6c8a95..20100018 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -1,16 +1,23 @@ import json -from django.shortcuts import get_object_or_404 +import logging + import rest_framework.exceptions -from rest_framework.generics import CreateAPIView, RetrieveAPIView, ListAPIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema from rest_framework.exceptions import PermissionDenied -from django.urls import reverse -from authentication.models import NetworkTypes, UserProfile, Wallet -from authentication.serializers import MessageResponseSerializer -from faucet.models import Chain, ClaimReceipt, GlobalSettings -from permissions.models import Permission +from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveAPIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response from rest_framework.views import APIView + +from authentication.models import NetworkTypes +from core.constraints import ( # noqa: F401 + BrightIDAuraVerification, + BrightIDMeetVerification, +) +from faucet.models import ClaimReceipt from tokenTap.models import TokenDistribution, TokenDistributionClaim from tokenTap.serializers import ( ConstraintSerializer, @@ -19,16 +26,18 @@ TokenDistributionClaimSerializer, TokenDistributionSerializer, ) -from django.utils import timezone -from drf_yasg.utils import swagger_auto_schema -from drf_yasg import openapi + +from .constraints import ( # noqa: F401 + ConstraintVerification, + OnceInALifeTimeVerification, + OncePerMonthVerification, +) from .helpers import ( create_uint32_random_nonce, + has_weekly_credit_left, hash_message, sign_hashed_message, - has_weekly_credit_left, ) -from .constraints import * class TokenDistributionListView(ListAPIView): @@ -38,9 +47,7 @@ class TokenDistributionListView(ListAPIView): def get_queryset(self): q = TokenDistribution.objects.filter(is_active=True) - sorted_queryset = sorted( - q, key=lambda obj: obj.total_claims_since_last_round, reverse=True - ) + sorted_queryset = sorted(q, key=lambda obj: obj.total_claims_since_last_round, reverse=True) return sorted_queryset @@ -50,9 +57,7 @@ class TokenDistributionClaimView(CreateAPIView): def check_token_distribution_is_claimable(self, token_distribution): if not token_distribution.is_claimable: - raise rest_framework.exceptions.PermissionDenied( - "This token is not claimable" - ) + raise rest_framework.exceptions.PermissionDenied("This token is not claimable") def check_user_permissions(self, token_distribution, user_profile): for c in token_distribution.permissions.all(): @@ -63,15 +68,11 @@ def check_user_permissions(self, token_distribution, user_profile): def check_user_weekly_credit(self, user_profile): if not has_weekly_credit_left(user_profile): - raise rest_framework.exceptions.PermissionDenied( - "You have reached your weekly claim limit" - ) + raise rest_framework.exceptions.PermissionDenied("You have reached your weekly claim limit") def check_user_has_wallet(self, user_profile): if not user_profile.wallets.filter(wallet_type=NetworkTypes.EVM).exists(): - raise rest_framework.exceptions.PermissionDenied( - "You have not connected an EVM wallet to your account" - ) + raise rest_framework.exceptions.PermissionDenied("You have not connected an EVM wallet to your account") @swagger_auto_schema( responses={ @@ -150,7 +151,7 @@ def post(self, request, *args, **kwargs): signature=lightning_invoice, token_distribution=token_distribution, ) - gas_tap_claim = ClaimReceipt.objects.create( + ClaimReceipt.objects.create( chain=token_distribution.chain, user_profile=user_profile, datetime=timezone.now(), @@ -172,11 +173,14 @@ class GetTokenDistributionConstraintsView(APIView): permission_classes = [IsAuthenticated] def get(self, request, td_id): + # from .constraints import + user_profile = request.user.profile td = get_object_or_404(TokenDistribution, pk=td_id) try: param_values = json.loads(td.constraint_params) - except: + except Exception as e: + logging.error("Error parsing constraint params", e) param_values = {} response_constraints = [] @@ -191,13 +195,9 @@ def get(self, request, td_id): is_verified = False if constraint.is_observed(token_distribution=td): is_verified = True - response_constraints.append( - {**ConstraintSerializer(c).data, "is_verified": is_verified} - ) + response_constraints.append({**ConstraintSerializer(c).data, "is_verified": is_verified}) - return Response( - {"success": True, "constraints": response_constraints}, status=200 - ) + return Response({"success": True, "constraints": response_constraints}, status=200) class TokenDistributionClaimStatusUpdateView(CreateAPIView): @@ -210,18 +210,12 @@ def post(self, request, *args, **kwargs): ) tx_hash = request.data.get("tx_hash", None) if tx_hash is None: - raise rest_framework.exceptions.ValidationError( - "tx_hash is a required field" - ) + raise rest_framework.exceptions.ValidationError("tx_hash is a required field") if token_distribution_claim.user_profile != user_profile: - raise rest_framework.exceptions.PermissionDenied( - "You do not have permission to update this claim" - ) + raise rest_framework.exceptions.PermissionDenied("You do not have permission to update this claim") if token_distribution_claim.status != ClaimReceipt.PENDING: - raise rest_framework.exceptions.PermissionDenied( - "This claim has already been updated" - ) + raise rest_framework.exceptions.PermissionDenied("This claim has already been updated") token_distribution_claim.tx_hash = tx_hash token_distribution_claim.status = ClaimReceipt.VERIFIED token_distribution_claim.save() @@ -247,6 +241,4 @@ class TokenDistributionClaimRetrieveView(RetrieveAPIView): def get_object(self): user_profile = self.request.user.profile - return TokenDistributionClaim.objects.get( - pk=self.kwargs["pk"], user_profile=user_profile - ) + return TokenDistributionClaim.objects.get(pk=self.kwargs["pk"], user_profile=user_profile)