diff --git a/core/utils.py b/core/utils.py index e074becb..21879d7e 100644 --- a/core/utils.py +++ b/core/utils.py @@ -114,3 +114,9 @@ def send_raw_tx(self, signed_tx): def wait_for_transaction_receipt(self, tx_hash): return self.w3.eth.wait_for_transaction_receipt(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/constraints.py b/faucet/constraints.py index e65009c1..457ddba6 100644 --- a/faucet/constraints.py +++ b/faucet/constraints.py @@ -1,4 +1,6 @@ +import requests from core.constraints import * +from core.utils import Web3Utils from .models import DonationReceipt, Chain, ClaimReceipt class DonationConstraint(ConstraintVerification): @@ -17,6 +19,50 @@ def is_observed(self, *args, **kwargs): class OptimismDonationConstraint(DonationConstraint): _param_keys = [] + def is_observed(self, *args, **kwargs): + try: + chain = Chain.objects.get(chain_id=10) + except: + return False + self._param_values[ConstraintParam.CHAIN] = chain.pk + return super().is_observed(*args, **kwargs) + +class EvmClaimingGasConstraint(ConstraintVerification): + _param_keys = [ + ConstraintParam.CHAIN + ] + + def is_observed(self, *args, **kwargs): + chain_pk = self._param_values[ConstraintParam.CHAIN] + 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 + + 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}" + ) + first_internal_tx = first_internal_tx.json() + if first_internal_tx and first_internal_tx['status'] == '1': + first_internal_tx = first_internal_tx['result'][0] + if first_internal_tx and first_internal_tx['from'] == chain.fund_manager_address.lower()\ + 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}" + ) + 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']) + web3_first_tx = w3.get_transaction_by_hash(first_tx['hash']) + return web3_first_tx['blockNumber'] > claiming_gas_tx['blockNumber'] + return False + +class OptimismClaimingGasConstraint(EvmClaimingGasConstraint): + _param_keys = [] + def is_observed(self, *args, **kwargs): try: chain = Chain.objects.get(chain_id=10) diff --git a/faucet/migrations/0059_chain_explorer_api_key_chain_explorer_api_url.py b/faucet/migrations/0059_chain_explorer_api_key_chain_explorer_api_url.py new file mode 100644 index 00000000..a564cc46 --- /dev/null +++ b/faucet/migrations/0059_chain_explorer_api_key_chain_explorer_api_url.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.4 on 2023-10-08 19:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0058_alter_claimreceipt_amount'), + ] + + operations = [ + migrations.AddField( + model_name='chain', + name='explorer_api_key', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='chain', + name='explorer_api_url', + field=models.URLField(blank=True, max_length=255, null=True), + ), + ] diff --git a/faucet/models.py b/faucet/models.py index 1f9a3123..f0fd8d17 100644 --- a/faucet/models.py +++ b/faucet/models.py @@ -207,6 +207,8 @@ class Chain(models.Model): decimals = models.IntegerField(default=18) explorer_url = models.URLField(max_length=255, blank=True, null=True) + explorer_api_url = models.URLField(max_length=255, blank=True, null=True) + explorer_api_key = models.CharField(max_length=255, blank=True, null=True) rpc_url = models.URLField(max_length=255, blank=True, null=True) logo_url = models.URLField(max_length=255, blank=True, null=True) modal_url = models.URLField(max_length=255, blank=True, null=True) diff --git a/faucet/test.py b/faucet/test.py index bfdf1fdb..8e5f5b9e 100644 --- a/faucet/test.py +++ b/faucet/test.py @@ -24,12 +24,15 @@ GlobalSettings, WalletAccount, TransactionBatch, - LightningConfig + LightningConfig, + Wallet, + NetworkTypes ) from unittest.mock import patch from dotenv import dotenv_values from faucet.helpers import memcache_lock from faucet.constants import * +from faucet.constraints import * address = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" fund_manager = "0x5802f1035AbB8B191bc12Ce4668E3815e8B7Efa0" @@ -646,3 +649,42 @@ def test_unclaimed(self): unclaimed = self.strategy.get_unclaimed() self.assertEqual(unclaimed, t_chain_max - 100) + + +class TestConstraints(APITestCase): + def setUp(self) -> None: + self.wallet = WalletAccount.objects.create( + name="Test Wallet", private_key=test_wallet_key + ) + + self.optimism = Chain.objects.create( + chain_name="Optimism", + native_currency_name="ETH", + symbol="ETH", + rpc_url_private="https://optimism.llamarpc.com", + wallet=self.wallet, + fund_manager_address="0xb3A97684Eb67182BAa7994b226e6315196D8b364", + chain_id=10, + max_claim_amount=t_chain_max, + explorer_url="https://optimistic.etherscan.io/", + explorer_api_url = "https://api-optimistic.etherscan.io", + explorer_api_key = "6PGF5HBTT7DG9CQCQZK3MWR9146JAWQKAC" + ) + + self.user_profile = create_new_user("0x5A73E32a77E04Fb3285608B0AdEaa000B8e248F2") + self.wallet = Wallet.objects.create( + user_profile=self.user_profile, + wallet_type=NetworkTypes.EVM, + address="0x5A73E32a77E04Fb3285608B0AdEaa000B8e248F2", + ) + self.client.force_authenticate(user=self.user_profile.user) + + def test_optimism_claiming_gas_contraint(self): + constraint = OptimismClaimingGasConstraint(self.user_profile) + self.assertTrue(constraint.is_observed()) + self.wallet.address = "0xE3eEBaB360E367b4e200759F0D955D1140F27430" + self.wallet.save() + self.assertTrue(constraint.is_observed()) + self.wallet.address = "0xB9e291b68E584be657477289389B3a6DEED3E34C" + self.wallet.save() + self.assertFalse(constraint.is_observed()) diff --git a/prizetap/migrations/0031_alter_constraint_name.py b/prizetap/migrations/0031_alter_constraint_name.py new file mode 100644 index 00000000..5efab84b --- /dev/null +++ b/prizetap/migrations/0031_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2023-10-09 10:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0030_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('BrightIDMeetVerification', 'BrightIDMeetVerification'), ('BrightIDAuraVerification', 'BrightIDAuraVerification'), ('HaveUnitapPass', 'HaveUnitapPass'), ('NotHaveUnitapPass', 'NotHaveUnitapPass'), ('OptimismDonationConstraint', 'OptimismDonationConstraint'), ('OptimismClaimingGasConstraint', 'OptimismClaimingGasConstraint')], max_length=255, unique=True), + ), + ] diff --git a/prizetap/models.py b/prizetap/models.py index 9eeefd45..c49e43b9 100644 --- a/prizetap/models.py +++ b/prizetap/models.py @@ -1,6 +1,6 @@ from django.db import models from faucet.models import Chain -from faucet.constraints import OptimismDonationConstraint +from faucet.constraints import OptimismDonationConstraint, OptimismClaimingGasConstraint from django.utils import timezone from django.utils.translation import gettext_lazy as _ from authentication.models import NetworkTypes, UserProfile @@ -14,7 +14,8 @@ class Constraint(UserConstraint): constraints = UserConstraint.constraints + [ HaveUnitapPass, NotHaveUnitapPass, - OptimismDonationConstraint + OptimismDonationConstraint, + OptimismClaimingGasConstraint ] name = UserConstraint.create_name_field(constraints)