diff --git a/api/lightning/node.py b/api/lightning/node.py index 98a98ff6..b21a565e 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -1,6 +1,5 @@ import hashlib import os -import random import secrets import time from base64 import b64decode @@ -129,7 +128,7 @@ class LNNode: } @classmethod - def pay_onchain(cls, onchainpayment, valid_code=1, on_mempool_code=2): + def pay_onchain(cls, onchainpayment, queue_code=5, on_mempool_code=2): """Send onchain transaction for buyer payouts""" if config("DISABLE_ONCHAIN", cast=bool): @@ -144,9 +143,12 @@ class LNNode: ) # Cheap security measure to ensure there has been some non-deterministic time between request and DB check - time.sleep(random.uniform(0.5, 10)) + delay = ( + secrets.randbelow(2**256) / (2**256) * 10 + ) # Random uniform 0 to 5 secs with good entropy + time.sleep(3 + delay) - if onchainpayment.status == valid_code: + if onchainpayment.status == queue_code: # Changing the state to "MEMPO" should be atomic with SendCoins. onchainpayment.status = on_mempool_code onchainpayment.save() diff --git a/api/logics.py b/api/logics.py index 0763eda5..a85aaea9 100644 --- a/api/logics.py +++ b/api/logics.py @@ -481,9 +481,9 @@ class Logics: "bad_request": "Only orders in dispute accept dispute statements" } - if len(statement) > 5000: + if len(statement) > 10000: return False, { - "bad_statement": "The statement is longer than 5000 characters" + "bad_statement": "The statement is longer than 10000 characters" } if len(statement) < 100: @@ -554,7 +554,7 @@ class Logics: confirmed = onchain_payment.balance.onchain_confirmed reserve = 300000 # We assume a reserve of 300K Sats (3 times higher than LND's default anchor reserve) pending_txs = OnchainPayment.objects.filter( - status=OnchainPayment.Status.VALID + status__in=[OnchainPayment.Status.VALID, OnchainPayment.Status.QUEUE] ).aggregate(Sum("num_satoshis"))["num_satoshis__sum"] if pending_txs is None: @@ -1049,13 +1049,6 @@ class Logics: safe_cltv_expiry_secs = cltv_expiry_secs * MAX_MINING_NETWORK_SPEEDUP_EXPECTED # Convert to blocks using assummed average block time (~8 mins/block) cltv_expiry_blocks = int(safe_cltv_expiry_secs / (BLOCK_TIME * 60)) - print( - invoice_concept, - " cltv_expiry_hours:", - cltv_expiry_secs / 3600, - " cltv_expiry_blocks:", - cltv_expiry_blocks, - ) return cltv_expiry_blocks @@ -1442,20 +1435,16 @@ class Logics: else: if not order.payout_tx.status == OnchainPayment.Status.VALID: return False - - valid = LNNode.pay_onchain( - order.payout_tx, - valid_code=OnchainPayment.Status.VALID, - on_mempool_code=OnchainPayment.Status.MEMPO, - ) - if valid: + else: + # Add onchain payment to queue order.status = Order.Status.SUC + order.payout_tx.status = OnchainPayment.Status.QUEUE + order.payout_tx.save() order.save() send_message.delay(order.id, "trade_successful") order.contract_finalization_time = timezone.now() order.save() return True - return False @classmethod def confirm_fiat(cls, order, user): diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py index dcefbea4..e7342fc5 100644 --- a/api/management/commands/follow_invoices.py +++ b/api/management/commands/follow_invoices.py @@ -8,7 +8,7 @@ from django.utils import timezone from api.lightning.node import LNNode from api.logics import Logics -from api.models import LNPayment, Order +from api.models import LNPayment, OnchainPayment, Order from api.tasks import follow_send_payment, send_message MACAROON = b64decode(config("LND_MACAROON_BASE64")) @@ -16,7 +16,7 @@ MACAROON = b64decode(config("LND_MACAROON_BASE64")) class Command(BaseCommand): - help = "Follows all active hold invoices" + help = "Follows all active hold invoices, sends out payments" rest = 5 # seconds between consecutive checks for invoice updates def handle(self, *args, **options): @@ -130,6 +130,14 @@ class Command(BaseCommand): self.stdout.write(str(debug)) def send_payments(self): + """ + Checks for invoices and onchain payments that are due to be paid. + Sends the payments. + """ + self.send_ln_payments() + self.send_onchain_payments() + + def send_ln_payments(self): """ Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts. Checks if any payment is due for retry, and tries to pay it. @@ -153,8 +161,36 @@ class Command(BaseCommand): queryset = queryset.union(queryset_retries) - for lnpayment in queryset: - follow_send_payment(lnpayment.payment_hash) + if len(queryset) > 0: + for lnpayment in queryset: + follow_send_payment.delay(lnpayment.payment_hash) + + def send_onchain_payments(self): + + queryset = OnchainPayment.objects.filter( + status=OnchainPayment.Status.QUEUE, + ) + + if len(queryset) > 0: + for onchainpayment in queryset: + # Checks that this onchain payment is part of an order with a settled escrow + if not hasattr(onchainpayment, "order_paid_TX"): + self.stdout.write( + f"Onchain payment {str(onchainpayment)} has no parent order!" + ) + return + order = onchainpayment.order_paid_TX + if order.trade_escrow.status == LNPayment.Status.SETLED: + # Sends out onchainpayment + LNNode.pay_onchain( + onchainpayment, + OnchainPayment.Status.QUEUE, + OnchainPayment.Status.MEMPO, + ) + else: + self.stdout.write( + f"Onchain payment {str(onchainpayment)} for order {str(order)} escrow is not settled!" + ) def update_order_status(self, lnpayment): """Background process following LND hold invoices diff --git a/api/models.py b/api/models.py index 5e9ab41d..94cc0f6c 100644 --- a/api/models.py +++ b/api/models.py @@ -185,10 +185,11 @@ class OnchainPayment(models.Model): class Status(models.IntegerChoices): CREAT = 0, "Created" # User was given platform fees and suggested mining fees - VALID = 1, "Valid" # Valid onchain address submitted + VALID = 1, "Valid" # Valid onchain address and fee submitted MEMPO = 2, "In mempool" # Tx is sent to mempool - CONFI = 3, "Confirmed" # Tx is confirme +2 blocks + CONFI = 3, "Confirmed" # Tx is confirmed +2 blocks CANCE = 4, "Cancelled" # Cancelled tx + QUEUE = 5, "Queued" # Payment is queued to be sent out def get_balance(): balance = BalanceLog.objects.create() @@ -441,10 +442,10 @@ class Order(models.Model): # in dispute is_disputed = models.BooleanField(default=False, null=False) maker_statement = models.TextField( - max_length=5000, null=True, default=None, blank=True + max_length=10000, null=True, default=None, blank=True ) taker_statement = models.TextField( - max_length=5000, null=True, default=None, blank=True + max_length=10000, null=True, default=None, blank=True ) # LNpayments diff --git a/api/serializers.py b/api/serializers.py index ded1e039..6124d0e4 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -501,7 +501,7 @@ class UpdateOrderSerializer(serializers.Serializer): max_length=100, allow_null=True, allow_blank=True, default=None ) statement = serializers.CharField( - max_length=10000, allow_null=True, allow_blank=True, default=None + max_length=11000, allow_null=True, allow_blank=True, default=None ) action = serializers.ChoiceField( choices=( diff --git a/api/tasks.py b/api/tasks.py index 4838e060..d53533b4 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -97,6 +97,7 @@ def follow_send_payment(hash): payment_request=lnpayment.invoice, fee_limit_sat=fee_limit_sat, timeout_seconds=timeout_seconds, + allow_self_payment=True, ) order = lnpayment.order_paid_LN diff --git a/api/views.py b/api/views.py index eb4dcc9b..894589e6 100644 --- a/api/views.py +++ b/api/views.py @@ -471,12 +471,15 @@ class OrderView(viewsets.ViewSet): if order.is_swap: data["num_satoshis"] = order.payout_tx.num_satoshis data["sent_satoshis"] = order.payout_tx.sent_satoshis + data["network"] = str(config("NETWORK")) if order.payout_tx.status in [ OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI, ]: data["txid"] = order.payout_tx.txid - data["network"] = str(config("NETWORK")) + elif order.payout_tx.status == OnchainPayment.Status.QUEUE: + data["tx_queued"] = True + data["address"] = order.payout_tx.address return Response(data, status.HTTP_200_OK) diff --git a/frontend/src/components/TradeBox/Prompts/Successful.tsx b/frontend/src/components/TradeBox/Prompts/Successful.tsx index b6c077f4..9bb97c61 100644 --- a/frontend/src/components/TradeBox/Prompts/Successful.tsx +++ b/frontend/src/components/TradeBox/Prompts/Successful.tsx @@ -11,6 +11,7 @@ import { Tooltip, IconButton, Button, + CircularProgress, } from '@mui/material'; import currencies from '../../../../static/assets/currencies.json'; import TradeSummary from '../TradeSummary'; @@ -150,6 +151,43 @@ export const SuccessfulPrompt = ({ + + + + + + {t('Sending coins to')} + + { + systemClient.copyToClipboard(order.address); + }} + > + + + + + + + {order.address} + + + + +