Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 52 additions & 47 deletions api/logics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import math
from datetime import timedelta
import typing

from decouple import config, Csv
from django.contrib.auth.models import User
Expand Down Expand Up @@ -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)

Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
15 changes: 4 additions & 11 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]:
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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 [
Expand Down
Loading