From f8c5a5ad872ad17dfcb4ca45158dab6555fe6505 Mon Sep 17 00:00:00 2001 From: Alice Coordinator Date: Sat, 24 Jan 2026 11:35:02 +0000 Subject: [PATCH 1/2] fix: log swap failure reason only once In the previous implementation, every time the payout_amount method was called, the swap failure reason was logged in the order. This method was called in the order GET method of the API, which led to logging the failure reason dozens of times for some orders. This commit splits the payout_amount calculation from the context generation. Now the logging happens only in the finalize_contract method, and this can only happen once. --- api/logics.py | 94 +++++++++++++++++++++++++++------------------------ api/views.py | 15 +++----- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/api/logics.py b/api/logics.py index 4dee2a8d1..b2c2c92f5 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,37 @@ 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.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}%" + order, buyer, preliminary_amount=context["invoice_amount"] ) - 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_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 + 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 + + return context @classmethod def escrow_amount(cls, order, user): @@ -812,7 +802,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 +887,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 +1348,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 [ From 0781c15ffdda62ad97b657b4972d067beb8294d0 Mon Sep 17 00:00:00 2001 From: Alice Coordinator Date: Fri, 3 Apr 2026 19:11:26 +0000 Subject: [PATCH 2/2] Fix error when order.payout_tx was not set --- api/logics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/logics.py b/api/logics.py index b2c2c92f5..f3bc72825 100644 --- a/api/logics.py +++ b/api/logics.py @@ -742,10 +742,11 @@ def compute_buyer_payout_context(cls, order) -> typing.Dict[str, typing.Any]: order, buyer, preliminary_amount=context["invoice_amount"] ) - context["suggested_mining_fee_rate"] = float( - order.payout_tx.suggested_mining_fee_rate - ) - context["swap_fee_rate"] = 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 + ) + context["swap_fee_rate"] = order.payout_tx.swap_fee_rate if not valid: context["swap_allowed"] = False