diff --git a/api/logics.py b/api/logics.py index 4dee2a8d1..f3bc72825 100644 --- a/api/logics.py +++ b/api/logics.py @@ -1,5 +1,6 @@ import math from datetime import timedelta +import typing from decouple import config, Csv from django.contrib.auth.models import User @@ -687,28 +688,36 @@ def create_onchain_payment(cls, order, user, preliminary_amount): return True @classmethod - def payout_amount(cls, order, user): + def payout_amount(cls, order) -> int: """Computes buyer invoice amount. Uses order.last_satoshis, 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: + if order.type == Order.Types.BUY: fee_fraction = FEE * MAKER_FEE_SPLIT - elif user == order.taker: + else: fee_fraction = FEE * (1 - MAKER_FEE_SPLIT) fee_sats = order.last_satoshis * fee_fraction - context = {} - # context necessary for the user to submit a LN invoice - context["invoice_amount"] = round( + return round( order.last_satoshis - fee_sats ) # Trading fee to buyer is charged here. + @classmethod + def compute_buyer_payout_context(cls, order) -> typing.Dict[str, typing.Any]: + """Computes the context necessary for the buyer to submit + either a LN invoice or an onchain address for payout.""" + context = {} + # context necessary for the user to submit a LN invoice + context["invoice_amount"] = cls.payout_amount(order) + # context necessary for the user to submit an onchain address + if config("DISABLE_ONCHAIN", cast=bool, default=True): + context["swap_allowed"] = False + context["swap_failure_reason"] = "On-the-fly submarine swaps are disabled" + return context + MIN_SWAP_AMOUNT = config("MIN_SWAP_AMOUNT", cast=int, default=20_000) MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500_000) @@ -717,56 +726,38 @@ def payout_amount(cls, order, user): context["swap_failure_reason"] = ( f"Order amount is smaller than the minimum swap available of {MIN_SWAP_AMOUNT} Sats" ) - order.log( - f"Onchain payment option was not offered: amount is smaller than the minimum swap available of {MIN_SWAP_AMOUNT} Sats", - level="WARN", - ) - return True, context + return context elif context["invoice_amount"] > MAX_SWAP_AMOUNT: context["swap_allowed"] = False context["swap_failure_reason"] = ( f"Order amount is bigger than the maximum swap available of {MAX_SWAP_AMOUNT} Sats" ) - order.log( - f"Onchain payment option was not offered: amount is bigger than the maximum swap available of {MAX_SWAP_AMOUNT} Sats", - level="WARN", - ) - return True, context - - if config("DISABLE_ONCHAIN", cast=bool, default=True): - context["swap_allowed"] = False - context["swap_failure_reason"] = "On-the-fly submarine swaps are disabled" - order.log( - "Onchain payment option was not offered: on-the-fly submarine swaps are disabled" - ) - return True, context + return context + valid = True if order.payout_tx is None: + buyer = order.maker if cls.is_buyer(order, order.maker) else order.taker # Creates the OnchainPayment object and checks node balance valid = cls.create_onchain_payment( - order, user, preliminary_amount=context["invoice_amount"] + order, buyer, preliminary_amount=context["invoice_amount"] ) - order.log( - f"Suggested mining fee is {order.payout_tx.suggested_mining_fee_rate} Sats/vbyte, the swap fee rate is {order.payout_tx.swap_fee_rate}%" + + if order.payout_tx is not None: + context["suggested_mining_fee_rate"] = float( + order.payout_tx.suggested_mining_fee_rate ) - if not valid: - context["swap_allowed"] = False - context["swap_failure_reason"] = ( - "Not enough onchain liquidity available to offer a swap" - ) - order.log( - "Onchain payment option was not offered: onchain liquidity available to offer a swap", - level="WARN", - ) - return True, context + context["swap_fee_rate"] = order.payout_tx.swap_fee_rate + + if not valid: + context["swap_allowed"] = False + context["swap_failure_reason"] = ( + "Not enough onchain liquidity available to offer a swap" + ) + return context context["swap_allowed"] = True - context["suggested_mining_fee_rate"] = float( - order.payout_tx.suggested_mining_fee_rate - ) - context["swap_fee_rate"] = order.payout_tx.swap_fee_rate - return True, context + return context @classmethod def escrow_amount(cls, order, user): @@ -812,7 +803,7 @@ def update_address(cls, order, user, address, mining_fee_rate): order.log(f"The address {address} is not valid", level="WARN") return False, context - num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"] + num_satoshis = cls.payout_amount(order) if mining_fee_rate: # not a valid mining fee min_mining_fee_rate = get_minning_fee("minimum", num_satoshis) @@ -897,7 +888,7 @@ def update_invoice(cls, order, user, invoice, routing_budget_ppm): # cancel onchain_payout if existing cls.cancel_onchain_payment(order) - num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"] + num_satoshis = cls.payout_amount(order) routing_budget_sats = float(num_satoshis) * ( float(routing_budget_ppm) / 1_000_000 ) @@ -1358,6 +1349,20 @@ def finalize_contract(cls, take_order): order.maker.robot.save(update_fields=["total_contracts"]) order.taker.robot.save(update_fields=["total_contracts"]) + context = Logics.compute_buyer_payout_context(order) + if "suggested_mining_fee_rate" in context and "swap_fee_rate" in context: + order.log( + f"Suggested mining fee is {context['suggested_mining_fee_rate']} Sats/vbyte, the swap fee rate is {context['swap_fee_rate']}%" + ) + + if not context["swap_allowed"]: + log_message = f"Onchain payment option was not offered: {context['swap_failure_reason']}" + + if config("DISABLE_ONCHAIN", cast=bool, default=True): + order.log(log_message) + else: + order.log(log_message, level="WARN") + take_order.delete() # Log a market tick diff --git a/api/views.py b/api/views.py index 877d2c957..50d169174 100644 --- a/api/views.py +++ b/api/views.py @@ -334,9 +334,7 @@ def get(self, request, format=None): ]["escrow_amount"] # Buyer sees the amount he receives elif data["is_buyer"]: - data["trade_satoshis"] = Logics.payout_amount(order, request.user)[ - 1 - ]["invoice_amount"] + data["trade_satoshis"] = Logics.payout_amount(order) # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice. if order.status == Order.Status.WFB and data["is_maker"]: @@ -389,11 +387,8 @@ def get(self, request, format=None): == order.taker_bond.status == LNPayment.Status.LOCKED ): - valid, context = Logics.payout_amount(order, request.user) - if valid: - data = {**data, **context} - else: - return Response(context, status.HTTP_400_BAD_REQUEST) + context = Logics.compute_buyer_payout_context(order) + data = {**data, **context} # 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]: @@ -451,9 +446,7 @@ def get(self, request, format=None): if order.payout.status == LNPayment.Status.EXPIRE: data["invoice_expired"] = True # Add invoice amount once again if invoice was expired. - data["trade_satoshis"] = Logics.payout_amount(order, request.user)[1][ - "invoice_amount" - ] + data["trade_satoshis"] = Logics.payout_amount(order) # 10) If status is 'Expired', "Sending", "Finished" or "failed routing", add info for renewal: elif order.status in [