diff --git a/core/admin.py b/core/admin.py index 19e93156..785080c8 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,16 +1,34 @@ from django.contrib import admin -from .models import TokenPrice +from .models import Chain, TokenPrice, WalletAccount class UserConstraintBaseAdmin(admin.ModelAdmin): - fields = ["name", "title", "type", "description", "explanation", "response", "icon_url"] + fields = [ + "name", + "title", + "type", + "description", + "explanation", + "response", + "icon_url", + ] list_display = ["pk", "name", "description"] +class WalletAccountAdmin(admin.ModelAdmin): + list_display = ["pk", "name", "address"] + + +class ChainAdmin(admin.ModelAdmin): + list_display = ["pk", "chain_name", "chain_id", "symbol", "chain_type"] + + class TokenPriceAdmin(admin.ModelAdmin): list_display = ["symbol", "usd_price", "price_url", "datetime", "last_updated"] list_filter = ["symbol"] +admin.site.register(WalletAccount, WalletAccountAdmin) +admin.site.register(Chain, ChainAdmin) admin.site.register(TokenPrice, TokenPriceAdmin) diff --git a/core/migrations/0004_walletaccount_chain.py b/core/migrations/0004_walletaccount_chain.py new file mode 100644 index 00000000..64f2df56 --- /dev/null +++ b/core/migrations/0004_walletaccount_chain.py @@ -0,0 +1,50 @@ +# Generated by Django 4.0.4 on 2023-12-03 08:08 + +from django.db import migrations, models +import django.db.models.deletion +import encrypted_model_fields.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_alter_tokenprice_price_url'), + ] + + operations = [ + migrations.CreateModel( + name='WalletAccount', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=255, null=True)), + ('private_key', encrypted_model_fields.fields.EncryptedCharField()), + ('network_type', models.CharField(choices=[('EVM', 'EVM'), ('Solana', 'Solana'), ('Lightning', 'Lightning'), ('NONEVM', 'NONEVM'), ('NONEVMXDC', 'NONEVMXDC')], default='EVM', max_length=10)), + ], + ), + migrations.CreateModel( + name='Chain', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('chain_name', models.CharField(max_length=255)), + ('chain_id', models.CharField(max_length=255, unique=True)), + ('native_currency_name', models.CharField(max_length=255)), + ('symbol', models.CharField(max_length=255)), + ('decimals', models.IntegerField(default=18)), + ('explorer_url', models.URLField(blank=True, max_length=255, null=True)), + ('explorer_api_url', models.URLField(blank=True, max_length=255, null=True)), + ('explorer_api_key', models.CharField(blank=True, max_length=255, null=True)), + ('rpc_url', models.URLField(blank=True, max_length=255, null=True)), + ('logo_url', models.URLField(blank=True, max_length=255, null=True)), + ('modal_url', models.URLField(blank=True, max_length=255, null=True)), + ('rpc_url_private', models.URLField(max_length=255)), + ('poa', models.BooleanField(default=False)), + ('max_gas_price', models.BigIntegerField(default=250000000000)), + ('gas_multiplier', models.FloatField(default=1)), + ('enough_fee_multiplier', models.BigIntegerField(default=200000)), + ('is_testnet', models.BooleanField(default=False)), + ('chain_type', models.CharField(choices=[('EVM', 'EVM'), ('Solana', 'Solana'), ('Lightning', 'Lightning'), ('NONEVM', 'NONEVM'), ('NONEVMXDC', 'NONEVMXDC')], default='EVM', max_length=10)), + ('is_active', models.BooleanField(default=True)), + ('wallet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='chains', to='core.walletaccount')), + ], + ), + ] diff --git a/core/migrations/0005_auto_20231203_0832.py b/core/migrations/0005_auto_20231203_0832.py new file mode 100644 index 00000000..1894e4cd --- /dev/null +++ b/core/migrations/0005_auto_20231203_0832.py @@ -0,0 +1,78 @@ +# Generated by Django 4.0.4 on 2023-12-03 08:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('faucet', '0066_rename_is_one_time_chain_is_one_time_claim'), + ('core', '0004_walletaccount_chain'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO core_walletaccount ( + id, + name, + private_key, + network_type + ) + SELECT + id, + name, + private_key, + network_type + FROM + faucet_walletaccount; + INSERT INTO core_chain ( + id, + chain_name, + symbol, + chain_id, + rpc_url, + decimals, + explorer_url, + logo_url, + native_currency_name, + poa, + rpc_url_private, + wallet_id, + modal_url, + max_gas_price, + gas_multiplier, + chain_type, + is_testnet, + is_active, + enough_fee_multiplier, + explorer_api_key, + explorer_api_url + ) + SELECT + id, + chain_name, + symbol, + chain_id, + rpc_url, + decimals, + explorer_url, + logo_url, + native_currency_name, + poa, + rpc_url_private, + wallet_id, + modal_url, + max_gas_price, + gas_multiplier, + chain_type, + is_testnet, + is_active, + enough_fee_multiplier, + explorer_api_key, + explorer_api_url + FROM + faucet_chain; + SELECT setval(pg_get_serial_sequence('"core_walletaccount"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "core_walletaccount"; + SELECT setval(pg_get_serial_sequence('"core_chain"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "core_chain"; + """, reverse_sql="") + ] diff --git a/core/models.py b/core/models.py index cf015857..efc6ab70 100644 --- a/core/models.py +++ b/core/models.py @@ -1,11 +1,58 @@ +import binascii import inspect +import logging +from bip_utils import Bip44, Bip44Coins from django.db import models from django.utils.translation import gettext_lazy as _ +from encrypted_model_fields.fields import EncryptedCharField +from solders.keypair import Keypair +from solders.pubkey import Pubkey + +from faucet.faucet_manager.lnpay_client import LNPayClient from .constraints import BrightIDAuraVerification, BrightIDMeetVerification +class NetworkTypes: + EVM = "EVM" + SOLANA = "Solana" + LIGHTNING = "Lightning" + NONEVM = "NONEVM" + NONEVMXDC = "NONEVMXDC" + + networks = ( + (EVM, "EVM"), + (SOLANA, "Solana"), + (LIGHTNING, "Lightning"), + (NONEVM, "NONEVM"), + (NONEVMXDC, "NONEVMXDC"), + ) + + +class BigNumField(models.Field): + empty_strings_allowed = False + + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 200 # or some other number + super().__init__(*args, **kwargs) + + def db_type(self, connection): + return "numeric" + + def get_internal_type(self): + return "BigNumField" + + def to_python(self, value): + if isinstance(value, str): + return int(value) + + return value + + def get_prep_value(self, value): + return str(value) + + class UserConstraint(models.Model): class Meta: abstract = True @@ -19,10 +66,15 @@ class Type(models.TextChoices): name = models.CharField( max_length=255, unique=True, - choices=[(f'{inspect.getmodule(c).__name__.split(".")[0]}.{c.__name__}', c.__name__) for c in constraints], + choices=[ + (f'{inspect.getmodule(c).__name__.split(".")[0]}.{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) explanation = models.TextField(null=True, blank=True) response = models.TextField(null=True, blank=True) @@ -36,7 +88,13 @@ def create_name_field(cls, constraints): return models.CharField( max_length=255, unique=True, - choices=[(f'{inspect.getmodule(c).__name__.split(".")[0]}.{c.__name__}', c.__name__) for c in constraints], + choices=[ + ( + f'{inspect.getmodule(c).__name__.split(".")[0]}.{c.__name__}', + c.__name__, + ) + for c in constraints + ], ) @@ -45,27 +103,143 @@ class TokenPrice(models.Model): 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, blank=True) - symbol = models.CharField(max_length=255, db_index=True, unique=True, null=False, blank=False) + symbol = models.CharField( + max_length=255, db_index=True, unique=True, null=False, blank=False + ) -class BigNumField(models.Field): - empty_strings_allowed = False +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 + ) - def __init__(self, *args, **kwargs): - kwargs["max_length"] = 200 # or some other number - super().__init__(*args, **kwargs) + @property + def address(self): + try: + node = Bip44.FromPrivateKey( + binascii.unhexlify(self.private_key), Bip44Coins.ETHEREUM + ) + return node.PublicKey().ToAddress() + except: # noqa: E722 + # dont change this, somehow it creates a bug if changed to Exception + try: + keypair = Keypair.from_base58_string(self.private_key) + return str(keypair.pubkey()) + except: # noqa: E722 + # dont change this, somehow it creates a bug if changed to Exception + pass - def db_type(self, connection): - return "numeric" + def __str__(self) -> str: + return "%s - %s" % (self.name, self.address) - def get_internal_type(self): - return "BigNumField" + @property + def main_key(self): + return self.private_key - def to_python(self, value): - if isinstance(value, str): - return int(value) - return value +class Chain(models.Model): + chain_name = models.CharField(max_length=255) + chain_id = models.CharField(max_length=255, unique=True) - def get_prep_value(self, value): - return str(value) + native_currency_name = models.CharField(max_length=255) + symbol = models.CharField(max_length=255) + 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) + rpc_url_private = models.URLField(max_length=255) + + poa = models.BooleanField(default=False) + + wallet = models.ForeignKey( + WalletAccount, related_name="chains", on_delete=models.PROTECT + ) + + max_gas_price = models.BigIntegerField(default=250000000000) + gas_multiplier = models.FloatField(default=1) + enough_fee_multiplier = models.BigIntegerField(default=200000) + + is_testnet = models.BooleanField(default=False) + chain_type = models.CharField( + max_length=10, choices=NetworkTypes.networks, default=NetworkTypes.EVM + ) + + is_active = models.BooleanField(default=True) + + def __str__(self): + return f"{self.chain_name} - {self.pk} - {self.symbol}:{self.chain_id}" + + @property + def wallet_balance(self): + return self.get_wallet_balance() + + def get_wallet_balance(self): + if not self.rpc_url_private: + return 0 + + try: + from faucet.faucet_manager.fund_manager import ( + EVMFundManager, + SolanaFundManager, + ) + + if self.chain_type == NetworkTypes.EVM or int(self.chain_id) == 500: + return EVMFundManager(self).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 + return v + elif self.chain_type == NetworkTypes.LIGHTNING: + lnpay_client = LNPayClient( + self.rpc_url_private, + self.wallet.main_key, + self.fund_manager_address, + ) + 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}" + ) + return 0 + + @property + def has_enough_fees(self): + if self.get_wallet_balance() > self.gas_price * self.enough_fee_multiplier: + return True + logging.warning(f"Chain {self.chain_name} has insufficient fees in wallet") + return False + + @property + def gas_price(self): + if not self.rpc_url_private: + return self.max_gas_price + 1 + + try: + from faucet.faucet_manager.fund_manager import EVMFundManager + + return EVMFundManager(self).get_gas_price() + except: # noqa: E722 + logging.exception(f"Error getting gas price for {self.chain_name}") + return self.max_gas_price + 1 + + @property + def is_gas_price_too_high(self): + if not self.rpc_url_private: + return True + + try: + from faucet.faucet_manager.fund_manager import EVMFundManager + + return EVMFundManager(self).is_gas_price_too_high + except Exception: # noqa: E722 + logging.exception(f"Error getting gas price for {self.chain_name}") + return True