diff --git a/api/lightning/node.py b/api/lightning/node.py index 8944746a..3eabb1b0 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -8,7 +8,7 @@ from base64 import b64decode from datetime import timedelta, datetime from django.utils import timezone -from api.models import LNPayment + ####### # Should work with LND (c-lightning in the future if there are features that deserve the work) @@ -176,6 +176,8 @@ class LNNode: @classmethod def validate_hold_invoice_locked(cls, lnpayment): """Checks if hold invoice is locked""" + from api.models import LNPayment + request = invoicesrpc.LookupInvoiceMsg( payment_hash=bytes.fromhex(lnpayment.payment_hash)) response = cls.invoicesstub.LookupInvoiceV2(request, @@ -296,7 +298,8 @@ class LNNode: @classmethod def pay_invoice(cls, lnpayment): """Sends sats. Used for rewards payouts""" - + from api.models import LNPayment + fee_limit_sat = int( max( lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), diff --git a/api/models.py b/api/models.py index b55651ff..c4bc7df6 100644 --- a/api/models.py +++ b/api/models.py @@ -17,8 +17,11 @@ from decouple import config from pathlib import Path import json +from control.models import BalanceLog + MIN_TRADE = int(config("MIN_TRADE")) MAX_TRADE = int(config("MAX_TRADE")) +MIN_SWAP_AMOUNT = int(config("MIN_SWAP_AMOUNT")) FEE = float(config("FEE")) DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE")) @@ -118,7 +121,7 @@ class LNPayment(models.Model): blank=True) num_satoshis = models.PositiveBigIntegerField(validators=[ MinValueValidator(100), - MaxValueValidator(MAX_TRADE * (1 + DEFAULT_BOND_SIZE + FEE)), + MaxValueValidator(1.5 * MAX_TRADE), ]) # Fee in sats with mSats decimals fee_msat fee = models.DecimalField(max_digits=10, decimal_places=3, default=0, null=False, blank=False) @@ -163,6 +166,96 @@ class LNPayment(models.Model): # We created a truncated property for display 'hash' return truncatechars(self.payment_hash, 10) +class OnchainPayment(models.Model): + + class Concepts(models.IntegerChoices): + PAYBUYER = 3, "Payment to buyer" + + class Status(models.IntegerChoices): + CREAT = 0, "Created" # User was given platform fees and suggested mining fees + VALID = 1, "Valid" # Valid onchain address submitted + MEMPO = 2, "In mempool" # Tx is sent to mempool + CONFI = 3, "Confirmed" # Tx is confirme +2 blocks + + # payment use details + concept = models.PositiveSmallIntegerField(choices=Concepts.choices, + null=False, + default=Concepts.PAYBUYER) + status = models.PositiveSmallIntegerField(choices=Status.choices, + null=False, + default=Status.VALID) + + # payment info + address = models.CharField(max_length=100, + unique=False, + default=None, + null=True, + blank=True) + + txid = models.CharField(max_length=64, + unique=True, + null=True, + default=None, + blank=True) + + num_satoshis = models.PositiveBigIntegerField(validators=[ + MinValueValidator(0.7 * MIN_SWAP_AMOUNT), + MaxValueValidator(1.5 * MAX_TRADE), + ]) + + # fee in sats/vbyte with mSats decimals fee_msat + suggested_mining_fee_rate = models.DecimalField(max_digits=6, + decimal_places=3, + default=1.05, + null=False, + blank=False) + mining_fee_rate = models.DecimalField(max_digits=6, + decimal_places=3, + default=1.05, + null=False, + blank=False) + mining_fee_sats = models.PositiveBigIntegerField(default=0, + null=False, + blank=False) + + # platform onchain/channels balance at creattion, swap fee rate as percent of total volume + node_balance = models.ForeignKey(BalanceLog, + related_name="balance", + on_delete=models.SET_NULL, + null=True) + swap_fee_rate = models.DecimalField(max_digits=4, + decimal_places=2, + default=2, + null=False, + blank=False) + + created_at = models.DateTimeField(default=timezone.now) + + # involved parties + receiver = models.ForeignKey(User, + related_name="tx_receiver", + on_delete=models.SET_NULL, + null=True, + default=None) + + def __str__(self): + if self.txid: + txname = str(self.txid)[:8] + else: + txname = str(self.id) + + return f"TX-{txname}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}" + + class Meta: + verbose_name = "Lightning payment" + verbose_name_plural = "Lightning payments" + + @property + def hash(self): + # Payment hash is the primary key of LNpayments + # However it is too long for the admin panel. + # We created a truncated property for display 'hash' + return truncatechars(self.payment_hash, 10) class Order(models.Model): diff --git a/api/tasks.py b/api/tasks.py index 7b869c77..f3a8839d 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -79,7 +79,7 @@ def follow_send_payment(hash): float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT")), )) # 1000 ppm or 10 sats - timeout_seconds = int(config("REWARRDS_TIMEOUT_SECONDS")) + timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS")) request = LNNode.routerrpc.SendPaymentRequest( payment_request=lnpayment.invoice, diff --git a/api/utils.py b/api/utils.py index f1109d1c..89205726 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,8 +1,7 @@ import requests, ring, os from decouple import config import numpy as np -import requests - +import coinaddrvalidator as addr from api.models import Order def get_tor_session(): @@ -12,6 +11,24 @@ def get_tor_session(): 'https': 'socks5://127.0.0.1:9050'} return session +def validate_onchain_address(address): + ''' + Validates an onchain address + ''' + + validation = addr.validate('btc', address.encode('utf-8')) + + if not validation.valid: + return False + + NETWORK = str(config('NETWORK')) + if NETWORK == 'mainnet': + if validation.network == 'main': + return True + elif NETWORK == 'testnet': + if validation.network == 'test': + return True + market_cache = {} @ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds def get_exchange_rates(currencies): diff --git a/api/views.py b/api/views.py index 3a8ab65a..b31c6de4 100644 --- a/api/views.py +++ b/api/views.py @@ -416,9 +416,10 @@ class OrderView(viewsets.ViewSet): order = Order.objects.get(id=order_id) # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' - # 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform' + # 5.b)'update_address' 6)'submit_statement' (in dispute), 7)'rate_user' , 8)'rate_platform' action = serializer.data.get("action") invoice = serializer.data.get("invoice") + address = serializer.data.get("address") statement = serializer.data.get("statement") rating = serializer.data.get("rating") @@ -464,6 +465,13 @@ class OrderView(viewsets.ViewSet): invoice) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) + + # 2.b) If action is 'update invoice' + if action == "update_address": + valid, context = Logics.update_address(order, request.user, + address) + if not valid: + return Response(context, status.HTTP_400_BAD_REQUEST) # 3) If action is cancel elif action == "cancel": diff --git a/requirements.txt b/requirements.txt index 43fe469d..cecb8980 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,5 @@ psycopg2==2.9.3 SQLAlchemy==1.4.31 django-import-export==2.7.1 requests[socks] -python-gnupg==0.4.9 \ No newline at end of file +python-gnupg==0.4.9 +coinaddrvalidator==1.1.3 \ No newline at end of file