From cf82a4d6ae59fa8b7e8d61894a3c1b6226d259d0 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Mon, 6 Jun 2022 13:37:51 -0700 Subject: [PATCH] Add onchain logics pt2 --- .env-sample | 4 +-- api/lightning/node.py | 2 +- api/logics.py | 61 +++++++++++++++++++++++++++++++++++++++---- api/models.py | 20 ++++++++++---- api/serializers.py | 6 +++++ api/tasks.py | 2 +- api/views.py | 3 ++- 7 files changed, 83 insertions(+), 15 deletions(-) diff --git a/.env-sample b/.env-sample index 0a37b7c7..f77a1df9 100644 --- a/.env-sample +++ b/.env-sample @@ -113,9 +113,9 @@ MAX_SWAP_FEE = 0.1 # Liquidity split point (LN/onchain) at which we use MAX_SWAP_FEE MAX_SWAP_POINT = 0 # Shape of fee to available liquidity curve. Only 'linear' implemented. -SWAP_FEE_ = 'linear' +SWAP_FEE_SHAPE = 'linear' # Min amount allowed for Swap -MIN_SWAP_AMOUNT = 800000 +MIN_SWAP_AMOUNT = 50000 # Reward tip. Reward for every finished trade in the referral program (Satoshis) REWARD_TIP = 100 diff --git a/api/lightning/node.py b/api/lightning/node.py index 3eabb1b0..ecc555a0 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -305,7 +305,7 @@ class LNNode: lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")), )) # 200 ppm or 10 sats - timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS")) + timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS")) request = routerrpc.SendPaymentRequest(payment_request=lnpayment.invoice, fee_limit_sat=fee_limit_sat, timeout_seconds=timeout_seconds) diff --git a/api/logics.py b/api/logics.py index 1e2d2808..5c52ef5b 100644 --- a/api/logics.py +++ b/api/logics.py @@ -4,7 +4,7 @@ from django.utils import timezone from api.lightning.node import LNNode from django.db.models import Q -from api.models import Order, LNPayment, MarketTick, User, Currency +from api.models import OnchainPayment, Order, LNPayment, MarketTick, User, Currency from api.tasks import send_message from decouple import config @@ -494,10 +494,46 @@ class Logics: order.save() return True, None + def compute_swap_fee_rate(balance): + shape = str(config('SWAP_FEE_SHAPE')) + + if shape == "linear": + MIN_SWAP_FEE = float(config('MIN_SWAP_FEE')) + MIN_POINT = float(config('MIN_POINT')) + MAX_SWAP_FEE = float(config('MAX_SWAP_FEE')) + MAX_POINT = float(config('MAX_POINT')) + if balance.onchain_fraction > MIN_POINT: + swap_fee_rate = MIN_SWAP_FEE + else: + slope = (MAX_SWAP_FEE - MIN_SWAP_FEE) / (MAX_POINT - MIN_POINT) + swap_fee_rate = slope * (balance.onchain_fraction - MAX_POINT) + MAX_SWAP_FEE + + return swap_fee_rate + + @classmethod + def create_onchain_payment(cls, order, estimate_sats): + ''' + Creates an empty OnchainPayment for order.payout_tx. + It sets the fees to be applied to this order if onchain Swap is used. + If the user submits a LN invoice instead. The returned OnchainPayment goes unused. + ''' + onchain_payment = OnchainPayment.objects.create() + onchain_payment.suggested_mining_fee_rate = LNNode.estimate_fee(amount_sats=estimate_sats) + onchain_payment.swap_fee_rate = cls.compute_swap_fee_rate(onchain_payment.balance) + onchain_payment.save() + + order.payout_tx = onchain_payment + order.save() + return True, None + @classmethod def payout_amount(cls, order, user): """Computes buyer invoice amount. Uses order.last_satoshis, - that is the final trade amount set at Taker Bond time""" + that is the final trade amount set at Taker Bond time + Adds context for onchain swap. + """ + if not cls.is_buyer(order, user): + return False, None if user == order.maker: fee_fraction = FEE * MAKER_FEE_SPLIT @@ -508,10 +544,25 @@ class Logics: reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0 - if cls.is_buyer(order, user): - invoice_amount = round(order.last_satoshis - fee_sats - reward_tip) # Trading fee to buyer is charged here. + context = {} + # context necessary for the user to submit a LN invoice + context["invoice_amount"] = round(order.last_satoshis - fee_sats - reward_tip) # Trading fee to buyer is charged here. - return True, {"invoice_amount": invoice_amount} + # context necessary for the user to submit an onchain address + MIN_SWAP_AMOUNT = int(config("MIN_SWAP_AMOUNT")) + + if context["invoice_amount"] < MIN_SWAP_AMOUNT: + context["swap_allowed"] = False + return True, context + + if order.payout_tx == None: + cls.create_onchain_payment(order, estimate_sats=context["invoice_amount"]) + + context["swap_allowed"] = True + context["suggested_mining_fee_rate"] = order.payout_tx.suggested_mining_fee_rate + context["swap_fee_rate"] = order.payout_tx.swap_fee_rate + + return True, context @classmethod def escrow_amount(cls, order, user): diff --git a/api/models.py b/api/models.py index c4bc7df6..b16ab10c 100644 --- a/api/models.py +++ b/api/models.py @@ -219,10 +219,11 @@ class OnchainPayment(models.Model): 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) + balance = models.ForeignKey(BalanceLog, + related_name="balance", + on_delete=models.SET_NULL, + default=BalanceLog.objects.create) + swap_fee_rate = models.DecimalField(max_digits=4, decimal_places=2, default=2, @@ -452,7 +453,16 @@ class Order(models.Model): # buyer payment LN invoice payout = models.OneToOneField( LNPayment, - related_name="order_paid", + related_name="order_paid_LN", + on_delete=models.SET_NULL, + null=True, + default=None, + blank=True, + ) + + payout_tx = models.OneToOneField( + OnchainPayment, + related_name="order_paid_TX", on_delete=models.SET_NULL, null=True, default=None, diff --git a/api/serializers.py b/api/serializers.py index 4001e225..fd088afd 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -54,6 +54,10 @@ class UpdateOrderSerializer(serializers.Serializer): allow_null=True, allow_blank=True, default=None) + address = serializers.CharField(max_length=100, + allow_null=True, + allow_blank=True, + default=None) statement = serializers.CharField(max_length=10000, allow_null=True, allow_blank=True, @@ -63,6 +67,7 @@ class UpdateOrderSerializer(serializers.Serializer): "pause", "take", "update_invoice", + "update_address", "submit_statement", "dispute", "cancel", @@ -79,6 +84,7 @@ class UpdateOrderSerializer(serializers.Serializer): default=None, ) amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None) + mining_fee_rate = serializers.DecimalField(max_digits=6, decimal_places=3, allow_null=True, required=False, default=None) class UserGenSerializer(serializers.Serializer): # Mandatory fields diff --git a/api/tasks.py b/api/tasks.py index f3a8839d..469fd1c0 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("REWARDS_TIMEOUT_SECONDS")) + timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS")) request = LNNode.routerrpc.SendPaymentRequest( payment_request=lnpayment.invoice, diff --git a/api/views.py b/api/views.py index b31c6de4..4425f0e2 100644 --- a/api/views.py +++ b/api/views.py @@ -420,6 +420,7 @@ class OrderView(viewsets.ViewSet): action = serializer.data.get("action") invoice = serializer.data.get("invoice") address = serializer.data.get("address") + mining_fee_rate = serializer.data.get("mining_fee_rate") statement = serializer.data.get("statement") rating = serializer.data.get("rating") @@ -469,7 +470,7 @@ class OrderView(viewsets.ViewSet): # 2.b) If action is 'update invoice' if action == "update_address": valid, context = Logics.update_address(order, request.user, - address) + address, mining_fee_rate) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)