mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 12:11:35 +00:00
feat(coordinator): add verbose order logs as table to admin (#764)
* add logs field * Log some order events * Add more logs. Format as table. * Add more logs * Add admin panel hyperlinks to order logs * Add lasts set of logs * Some fixes
This commit is contained in:
parent
9777a4987f
commit
4383d14f88
22
api/admin.py
22
api/admin.py
@ -3,12 +3,14 @@ from statistics import median
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.utils.html import format_html
|
||||
from django_admin_relation_links import AdminChangeLinksMixin
|
||||
from rest_framework.authtoken.admin import TokenAdmin
|
||||
from rest_framework.authtoken.models import TokenProxy
|
||||
|
||||
from api.logics import Logics
|
||||
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order, Robot
|
||||
from api.utils import objects_to_hyperlinks
|
||||
|
||||
admin.site.unregister(Group)
|
||||
admin.site.unregister(User)
|
||||
@ -124,7 +126,11 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"min_amount",
|
||||
"max_amount",
|
||||
]
|
||||
readonly_fields = ["reference"]
|
||||
readonly_fields = ("reference", "_logs")
|
||||
|
||||
def _logs(self, obj):
|
||||
with_hyperlinks = objects_to_hyperlinks(obj.logs)
|
||||
return format_html(f'<table style="width: 100%">{with_hyperlinks}</table>')
|
||||
|
||||
actions = [
|
||||
"maker_wins",
|
||||
@ -156,8 +162,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
|
||||
order.maker.robot.earned_rewards = own_bond_sats + trade_sats
|
||||
order.maker.robot.save(update_fields=["earned_rewards"])
|
||||
order.status = Order.Status.TLD
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.TLD)
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
@ -195,8 +200,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
order.taker.robot.earned_rewards = own_bond_sats + trade_sats
|
||||
order.taker.robot.save(update_fields=["earned_rewards"])
|
||||
|
||||
order.status = Order.Status.MLD
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.MLD)
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
@ -236,8 +240,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
)
|
||||
order.trade_escrow.sender.robot.save(update_fields=["earned_rewards"])
|
||||
|
||||
order.status = Order.Status.CCA
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.CCA)
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
@ -271,10 +274,9 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
if order.is_swap:
|
||||
order.payout_tx.status = OnchainPayment.Status.VALID
|
||||
order.payout_tx.save(update_fields=["status"])
|
||||
order.status = Order.Status.SUC
|
||||
order.update_status(Order.Status.SUC)
|
||||
else:
|
||||
order.status = Order.Status.PAY
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.PAY)
|
||||
|
||||
Logics.pay_buyer(order)
|
||||
|
||||
|
@ -36,7 +36,6 @@ MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500000)
|
||||
|
||||
|
||||
class CLNNode:
|
||||
|
||||
os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
|
||||
|
||||
# Create the SSL credentials object
|
||||
@ -563,8 +562,7 @@ class CLNNode:
|
||||
lnpayment.in_flight = True
|
||||
lnpayment.save(update_fields=["in_flight", "status"])
|
||||
|
||||
order.status = Order.Status.PAY
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.PAY)
|
||||
|
||||
response = cls.stub.Pay(request)
|
||||
|
||||
@ -594,14 +592,19 @@ class CLNNode:
|
||||
]
|
||||
)
|
||||
|
||||
order.status = Order.Status.FAI
|
||||
order.update_status(Order.Status.FAI)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.FAI)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
|
||||
print(
|
||||
f"Order: {order.id} FAILED. Hash: {hash} Reason: {cls.payment_failure_context[-1]}"
|
||||
)
|
||||
order.log(
|
||||
f"Payment LNPayment({lnpayment.payment_hash},{str(lnpayment)}) failed. Failure reason: {cls.payment_failure_context[-1]})"
|
||||
)
|
||||
|
||||
return {
|
||||
"succeded": False,
|
||||
"context": f"payment failure reason: {cls.payment_failure_context[-1]}",
|
||||
@ -618,11 +621,15 @@ class CLNNode:
|
||||
)
|
||||
lnpayment.preimage = response.payment_preimage.hex()
|
||||
lnpayment.save(update_fields=["status", "fee", "preimage"])
|
||||
order.status = Order.Status.SUC
|
||||
order.update_status(Order.Status.SUC)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.SUC)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
|
||||
order.log(
|
||||
f"Payment LNPayment({lnpayment.payment_hash},{str(lnpayment)}) <b>succeeded</b>"
|
||||
)
|
||||
|
||||
results = {"succeded": True}
|
||||
return results
|
||||
@ -665,14 +672,19 @@ class CLNNode:
|
||||
]
|
||||
)
|
||||
|
||||
order.status = Order.Status.FAI
|
||||
order.update_status(Order.Status.FAI)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.FAI)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
|
||||
print(
|
||||
f"Order: {order.id} FAILED. Hash: {hash} Reason: {cls.payment_failure_context[status_code]}"
|
||||
)
|
||||
order.log(
|
||||
f"Payment LNPayment({lnpayment.payment_hash},{str(lnpayment)}) <b>failed</b>. Failure reason: {cls.payment_failure_context[status_code]}"
|
||||
)
|
||||
|
||||
return {
|
||||
"succeded": False,
|
||||
"context": f"payment failure reason: {cls.payment_failure_context[status_code]}",
|
||||
@ -700,11 +712,16 @@ class CLNNode:
|
||||
"in_flight",
|
||||
]
|
||||
)
|
||||
order.status = Order.Status.FAI
|
||||
order.update_status(Order.Status.FAI)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.FAI)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
|
||||
order.log(
|
||||
f"Payment LNPayment({lnpayment.payment_hash},{str(lnpayment)}) <b>had expired</b>"
|
||||
)
|
||||
|
||||
results = {
|
||||
"succeded": False,
|
||||
"context": "The payout invoice has expired",
|
||||
|
@ -46,7 +46,6 @@ MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500_000)
|
||||
|
||||
|
||||
class LNDNode:
|
||||
|
||||
os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
|
||||
|
||||
def metadata_callback(context, callback):
|
||||
@ -170,10 +169,17 @@ class LNDNode:
|
||||
onchainpayment.txid = response.txid
|
||||
onchainpayment.broadcasted = True
|
||||
onchainpayment.save(update_fields=["txid", "broadcasted"])
|
||||
onchainpayment.order_paid_TX.log(
|
||||
f"TX OnchainPayment({onchainpayment.id},{response.txid}) in <b>mempool</b>"
|
||||
)
|
||||
return True
|
||||
|
||||
elif onchainpayment.status == on_mempool_code:
|
||||
# Bug, double payment attempted
|
||||
onchainpayment.order_paid_TX.log(
|
||||
f"Attempted to re-broadcast OnchainPayment({onchainpayment.id},{onchainpayment}) already in mempool",
|
||||
level="ERROR",
|
||||
)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@ -193,7 +199,16 @@ class LNDNode:
|
||||
return str(response) == "" # True if no response, false otherwise.
|
||||
|
||||
@classmethod
|
||||
def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry, cltv_expiry_blocks, order_id, lnpayment_concept, time):
|
||||
def gen_hold_invoice(
|
||||
cls,
|
||||
num_satoshis,
|
||||
description,
|
||||
invoice_expiry,
|
||||
cltv_expiry_blocks,
|
||||
order_id,
|
||||
lnpayment_concept,
|
||||
time,
|
||||
):
|
||||
"""Generates hold invoice"""
|
||||
|
||||
hold_payment = {}
|
||||
@ -422,7 +437,6 @@ class LNDNode:
|
||||
)
|
||||
|
||||
for response in cls.routerstub.SendPaymentV2(request):
|
||||
|
||||
if (
|
||||
response.status == lnrpc.Payment.PaymentStatus.UNKNOWN
|
||||
): # Status 0 'UNKNOWN'
|
||||
@ -487,7 +501,7 @@ class LNDNode:
|
||||
lnpayment.status = LNPayment.Status.FLIGHT
|
||||
lnpayment.in_flight = True
|
||||
lnpayment.save(update_fields=["in_flight", "status"])
|
||||
order.status = Order.Status.PAY
|
||||
order.update_status(Order.Status.PAY)
|
||||
order.save(update_fields=["status"])
|
||||
|
||||
if (
|
||||
@ -532,14 +546,23 @@ class LNDNode:
|
||||
]
|
||||
)
|
||||
|
||||
order.status = Order.Status.FAI
|
||||
order.update_status(Order.Status.FAI)
|
||||
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.FAI)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
|
||||
str_failure_reason = cls.payment_failure_context[
|
||||
response.failure_reason
|
||||
]
|
||||
print(
|
||||
f"Order: {order.id} FAILED. Hash: {hash} Reason: {cls.payment_failure_context[response.failure_reason]}"
|
||||
f"Order: {order.id} FAILED. Hash: {hash} Reason: {str_failure_reason}"
|
||||
)
|
||||
order.log(
|
||||
f"Payment LNPayment({lnpayment.payment_hash},{str(lnpayment)}) <b>failed</b>. Failure reason: {str_failure_reason})"
|
||||
)
|
||||
|
||||
return {
|
||||
"succeded": False,
|
||||
"context": f"payment failure reason: {cls.payment_failure_context[response.failure_reason]}",
|
||||
@ -554,22 +577,24 @@ class LNDNode:
|
||||
lnpayment.preimage = response.payment_preimage
|
||||
lnpayment.save(update_fields=["status", "fee", "preimage"])
|
||||
|
||||
order.status = Order.Status.SUC
|
||||
order.update_status(Order.Status.SUC)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.SUC)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
|
||||
order.log(
|
||||
f"Payment LNPayment({lnpayment.payment_hash},{str(lnpayment)}) <b>succeeded</b>"
|
||||
)
|
||||
|
||||
results = {"succeded": True}
|
||||
return results
|
||||
|
||||
try:
|
||||
for response in cls.routerstub.SendPaymentV2(request):
|
||||
|
||||
handle_response(response)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
if "invoice expired" in str(e):
|
||||
print(f"Order: {order.id}. INVOICE EXPIRED. Hash: {hash}")
|
||||
# An expired invoice can already be in-flight. Check.
|
||||
@ -594,11 +619,15 @@ class LNDNode:
|
||||
update_fields=["status", "last_routing_time", "in_flight"]
|
||||
)
|
||||
|
||||
order.status = Order.Status.FAI
|
||||
order.update_status(Order.Status.FAI)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.FAI)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
|
||||
order.log(
|
||||
f"Payment LNPayment({lnpayment.payment_hash},{str(lnpayment)}) <b>had expired</b>"
|
||||
)
|
||||
|
||||
results = {
|
||||
"succeded": False,
|
||||
|
361
api/logics.py
361
api/logics.py
@ -168,11 +168,14 @@ class Logics:
|
||||
if order.has_range:
|
||||
order.amount = amount
|
||||
order.taker = user
|
||||
order.status = Order.Status.TAK
|
||||
order.update_status(Order.Status.TAK)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.TAK)
|
||||
)
|
||||
order.save(update_fields=["amount", "taker", "status", "expires_at"])
|
||||
order.save(update_fields=["amount", "taker", "expires_at"])
|
||||
order.log(
|
||||
f"Taken by Robot({user.robot.id},{user.username}) for {order.amount} fiat units"
|
||||
)
|
||||
return True, None
|
||||
|
||||
def is_buyer(order, user):
|
||||
@ -204,7 +207,6 @@ class Logics:
|
||||
satoshis_now = cls.calc_sats(
|
||||
amount, order.currency.exchange_rate, order.premium
|
||||
)
|
||||
|
||||
return int(satoshis_now)
|
||||
|
||||
def price_and_premium_now(order):
|
||||
@ -253,23 +255,35 @@ class Logics:
|
||||
return False
|
||||
|
||||
elif order.status == Order.Status.WFB:
|
||||
order.status = Order.Status.EXP
|
||||
order.update_status(Order.Status.EXP)
|
||||
order.expiry_reason = Order.ExpiryReasons.NMBOND
|
||||
cls.cancel_bond(order.maker_bond)
|
||||
order.save(update_fields=["status", "expiry_reason"])
|
||||
order.save(update_fields=["expiry_reason"])
|
||||
|
||||
order.log("Order expired while waiting for maker bond")
|
||||
order.log("Maker bond was cancelled")
|
||||
|
||||
return True
|
||||
|
||||
elif order.status in [Order.Status.PUB, Order.Status.PAU]:
|
||||
cls.return_bond(order.maker_bond)
|
||||
order.status = Order.Status.EXP
|
||||
order.update_status(Order.Status.EXP)
|
||||
order.expiry_reason = Order.ExpiryReasons.NTAKEN
|
||||
order.save(update_fields=["status", "expiry_reason"])
|
||||
order.save(update_fields=["expiry_reason"])
|
||||
send_notification.delay(order_id=order.id, message="order_expired_untaken")
|
||||
|
||||
order.log("Order expired while public or paused")
|
||||
order.log("Maker bond was <b>unlocked</b>")
|
||||
|
||||
return True
|
||||
|
||||
elif order.status == Order.Status.TAK:
|
||||
cls.cancel_bond(order.taker_bond)
|
||||
cls.kick_taker(order)
|
||||
|
||||
order.log("Order expired while waiting for taker bond")
|
||||
order.log("Taker bond was cancelled")
|
||||
|
||||
return True
|
||||
|
||||
elif order.status == Order.Status.WF2:
|
||||
@ -281,9 +295,16 @@ class Logics:
|
||||
cls.settle_bond(order.maker_bond)
|
||||
cls.settle_bond(order.taker_bond)
|
||||
cls.cancel_escrow(order)
|
||||
order.status = Order.Status.EXP
|
||||
order.update_status(Order.Status.EXP)
|
||||
order.expiry_reason = Order.ExpiryReasons.NESINV
|
||||
order.save(update_fields=["status", "expiry_reason"])
|
||||
order.save(update_fields=["expiry_reason"])
|
||||
|
||||
order.log(
|
||||
"Order expired while waiting for both buyer invoice and seller escrow"
|
||||
)
|
||||
order.log("Maker bond was <b>settled</b>")
|
||||
order.log("Taker bond was <b>settled</b>")
|
||||
|
||||
return True
|
||||
|
||||
elif order.status == Order.Status.WFE:
|
||||
@ -297,11 +318,16 @@ class Logics:
|
||||
cls.cancel_escrow(order)
|
||||
except Exception:
|
||||
pass
|
||||
order.status = Order.Status.EXP
|
||||
order.update_status(Order.Status.EXP)
|
||||
order.expiry_reason = Order.ExpiryReasons.NESCRO
|
||||
order.save(update_fields=["status", "expiry_reason"])
|
||||
order.save(update_fields=["expiry_reason"])
|
||||
# Reward taker with part of the maker bond
|
||||
cls.add_slashed_rewards(order, order.maker_bond, order.taker_bond)
|
||||
|
||||
order.log("Order expired while waiting for escrow of the maker/seller")
|
||||
order.log("Maker bond was <b>settled</b>")
|
||||
order.log("Taker bond was <b>unlocked</b>")
|
||||
|
||||
return True
|
||||
|
||||
# If maker is buyer, settle the taker's bond order goes back to public
|
||||
@ -317,6 +343,10 @@ class Logics:
|
||||
send_notification.delay(order_id=order.id, message="order_published")
|
||||
# Reward maker with part of the taker bond
|
||||
cls.add_slashed_rewards(order, taker_bond, order.maker_bond)
|
||||
|
||||
order.log("Order expired while waiting for escrow of the taker/seller")
|
||||
order.log("Taker bond was <b>settled</b>")
|
||||
|
||||
return True
|
||||
|
||||
elif order.status == Order.Status.WFI:
|
||||
@ -329,11 +359,16 @@ class Logics:
|
||||
cls.settle_bond(order.maker_bond)
|
||||
cls.return_bond(order.taker_bond)
|
||||
cls.return_escrow(order)
|
||||
order.status = Order.Status.EXP
|
||||
order.update_status(Order.Status.EXP)
|
||||
order.expiry_reason = Order.ExpiryReasons.NINVOI
|
||||
order.save(update_fields=["status", "expiry_reason"])
|
||||
order.save(update_fields=["expiry_reason"])
|
||||
# Reward taker with part of the maker bond
|
||||
cls.add_slashed_rewards(order, order.maker_bond, order.taker_bond)
|
||||
|
||||
order.log("Order expired while waiting for invoice of the maker/buyer")
|
||||
order.log("Maker bond was <b>settled</b>")
|
||||
order.log("Taker bond was <b>unlocked</b>")
|
||||
|
||||
return True
|
||||
|
||||
# If maker is seller settle the taker's bond, order goes back to public
|
||||
@ -345,6 +380,10 @@ class Logics:
|
||||
send_notification.delay(order_id=order.id, message="order_published")
|
||||
# Reward maker with part of the taker bond
|
||||
cls.add_slashed_rewards(order, taker_bond, order.maker_bond)
|
||||
|
||||
order.log("Order expired while waiting for invoice of the taker/buyer")
|
||||
order.log("Taker bond was <b>settled</b>")
|
||||
|
||||
return True
|
||||
|
||||
elif order.status in [Order.Status.CHA, Order.Status.FSE]:
|
||||
@ -352,6 +391,9 @@ class Logics:
|
||||
# was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
|
||||
# sent", we assume this is a dispute case by default.
|
||||
cls.open_dispute(order)
|
||||
order.log(
|
||||
"Order expired during chat and a dispute was opened automatically"
|
||||
)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@ -367,6 +409,8 @@ class Logics:
|
||||
|
||||
# Make order public again
|
||||
cls.publish_order(order)
|
||||
|
||||
order.log("Taker was kicked out of the order")
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@ -401,21 +445,39 @@ class Logics:
|
||||
cls.return_escrow(order)
|
||||
cls.settle_bond(order.maker_bond)
|
||||
cls.settle_bond(order.taker_bond)
|
||||
order.status = Order.Status.DIS
|
||||
order.update_status(Order.Status.DIS)
|
||||
|
||||
order.log("Maker bond was <b>settled</b>")
|
||||
order.log("Taker bond was <b>settled</b>")
|
||||
order.log(
|
||||
"No robot wrote in the chat, the dispute cannot be solved automatically"
|
||||
)
|
||||
|
||||
elif num_messages_maker == 0:
|
||||
cls.return_escrow(order)
|
||||
cls.settle_bond(order.maker_bond)
|
||||
cls.return_bond(order.taker_bond)
|
||||
cls.add_slashed_rewards(order, order.maker_bond, order.taker_bond)
|
||||
order.status = Order.Status.MLD
|
||||
order.update_status(Order.Status.MLD)
|
||||
|
||||
order.log("Maker bond was <b>settled</b>")
|
||||
order.log("Taker bond was <b>unlocked</b>")
|
||||
order.log(
|
||||
"<b>The dispute was solved automatically:</b> 'Maker lost dispute', the maker did not write in the chat"
|
||||
)
|
||||
|
||||
elif num_messages_taker == 0:
|
||||
cls.return_escrow(order)
|
||||
cls.settle_bond(order.taker_bond)
|
||||
cls.return_bond(order.maker_bond)
|
||||
cls.add_slashed_rewards(order, order.taker_bond, order.maker_bond)
|
||||
order.status = Order.Status.TLD
|
||||
order.update_status(Order.Status.TLD)
|
||||
|
||||
order.log("Maker bond was <b>unlocked</b>")
|
||||
order.log("Taker bond was <b>settled</b>")
|
||||
order.log(
|
||||
"<b>The dispute was solved automatically:</b> 'Taker lost dispute', the maker did not write in the chat"
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
@ -423,14 +485,13 @@ class Logics:
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.DIS)
|
||||
)
|
||||
order.save(update_fields=["status", "is_disputed", "expires_at"])
|
||||
order.save(update_fields=["is_disputed", "expires_at"])
|
||||
send_notification.delay(order_id=order.id, message="dispute_opened")
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def open_dispute(cls, order, user=None):
|
||||
|
||||
# Always settle escrow and bonds during a dispute. Disputes
|
||||
# can take long to resolve, it might trigger force closure
|
||||
# for unresolved HTLCs) Dispute winner will have to submit a
|
||||
@ -457,13 +518,13 @@ class Logics:
|
||||
cls.settle_bond(order.taker_bond)
|
||||
|
||||
order.is_disputed = True
|
||||
order.status = Order.Status.DIS
|
||||
order.update_status(Order.Status.DIS)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.DIS)
|
||||
)
|
||||
order.save(update_fields=["is_disputed", "status", "expires_at"])
|
||||
order.save(update_fields=["is_disputed", "expires_at"])
|
||||
|
||||
# User could be None if a dispute is open automatically due to weird expiration.
|
||||
# User could be None if a dispute is open automatically due to time expiration.
|
||||
if user is not None:
|
||||
robot = user.robot
|
||||
robot.num_disputes = robot.num_disputes + 1
|
||||
@ -476,6 +537,12 @@ class Logics:
|
||||
robot.save(update_fields=["num_disputes", "orders_disputes_started"])
|
||||
|
||||
send_notification.delay(order_id=order.id, message="dispute_opened")
|
||||
order.log(
|
||||
f"Dispute was opened {f'by Robot({user.robot.id},{user.username})' if user else ''}"
|
||||
)
|
||||
order.log("Maker bond was <b>settled</b>")
|
||||
order.log("Taker bond was <b>settled</b>")
|
||||
|
||||
return True, None
|
||||
|
||||
def dispute_statement(order, user, statement):
|
||||
@ -508,16 +575,18 @@ class Logics:
|
||||
None,
|
||||
"",
|
||||
]:
|
||||
order.status = Order.Status.WFR
|
||||
order.update_status(Order.Status.WFR)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.WFR)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
|
||||
order.log(
|
||||
f"Dispute statement submitted by Robot({user.robot.id},{user.username}) with length of {len(statement)} chars"
|
||||
)
|
||||
return True, None
|
||||
|
||||
def compute_swap_fee_rate(balance):
|
||||
|
||||
shape = str(config("SWAP_FEE_SHAPE"))
|
||||
|
||||
if shape == "linear":
|
||||
@ -591,6 +660,11 @@ class Logics:
|
||||
|
||||
order.payout_tx = onchain_payment
|
||||
order.save(update_fields=["payout_tx"])
|
||||
|
||||
order.log(
|
||||
f"Empty OnchainPayment({order.payout_tx.id},{order.payout_tx}) was created. Available onchain balance is {available_onchain} Sats"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@ -624,17 +698,28 @@ class Logics:
|
||||
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
|
||||
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 dissabled"
|
||||
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
|
||||
|
||||
if order.payout_tx is None:
|
||||
@ -642,11 +727,18 @@ class Logics:
|
||||
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}%"
|
||||
)
|
||||
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
|
||||
@ -676,10 +768,9 @@ class Logics:
|
||||
|
||||
@classmethod
|
||||
def update_address(cls, order, user, address, mining_fee_rate):
|
||||
|
||||
# Empty address?
|
||||
if not address:
|
||||
return False, {"bad_address": "You submitted an empty invoice"}
|
||||
return False, {"bad_address": "You submitted an empty address"}
|
||||
# only the buyer can post a buyer address
|
||||
if not cls.is_buyer(order, user):
|
||||
return False, {
|
||||
@ -691,10 +782,15 @@ class Logics:
|
||||
== order.maker_bond.status
|
||||
== LNPayment.Status.LOCKED
|
||||
) or order.status not in [Order.Status.WFI, Order.Status.WF2]:
|
||||
order.log(
|
||||
f"Robot({user.robot.id},{user.username}) attempted to submit an address while the order was in status {order.status}",
|
||||
level="ERROR",
|
||||
)
|
||||
return False, {"bad_request": "You cannot submit an address now."}
|
||||
# not a valid address
|
||||
valid, context = validate_onchain_address(address)
|
||||
if not valid:
|
||||
order.log(f"The address {address} is not valid", level="WARN")
|
||||
return False, context
|
||||
|
||||
num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
|
||||
@ -708,10 +804,18 @@ class Logics:
|
||||
min_mining_fee_rate = max(2, min_mining_fee_rate)
|
||||
|
||||
if float(mining_fee_rate) < min_mining_fee_rate:
|
||||
order.log(
|
||||
f"The onchain fee {float(mining_fee_rate)} Sats/vbytes proposed by Robot({user.robot.id},{user.username}) is less than the current minimum mining fee {min_mining_fee_rate} Sats",
|
||||
level="WARN",
|
||||
)
|
||||
return False, {
|
||||
"bad_address": f"The mining fee is too low. Must be higher than {min_mining_fee_rate} Sat/vbyte"
|
||||
}
|
||||
elif float(mining_fee_rate) > 500:
|
||||
order.log(
|
||||
f"The onchain fee {float(mining_fee_rate)} Sats/vbytes proposed by Robot({user.robot.id},{user.username}) is higher than the absolute maximum mining fee 500 Sats",
|
||||
level="WARN",
|
||||
)
|
||||
return False, {
|
||||
"bad_address": "The mining fee is too high, must be less than 500 Sats/vbyte"
|
||||
}
|
||||
@ -731,6 +835,10 @@ class Logics:
|
||||
)
|
||||
|
||||
if float(tx.sent_satoshis) < 20_000:
|
||||
order.log(
|
||||
f"The onchain Sats to be sent ({float(tx.sent_satoshis)}) are below the dust limit of 20,000 Sats",
|
||||
level="WARN",
|
||||
)
|
||||
return False, {
|
||||
"bad_address": "The amount remaining after subtracting mining fee is close to dust limit."
|
||||
}
|
||||
@ -740,15 +848,21 @@ class Logics:
|
||||
order.is_swap = True
|
||||
order.save(update_fields=["is_swap"])
|
||||
|
||||
order.log(
|
||||
f"Robot({user.robot.id},{user.username}) added an onchain address OnchainPayment({tx.id},{address[:6]}...{address[-4:]}) as payout method. Amount to be sent is {tx.sent_satoshis} Sats, mining fee is {tx.mining_fee_sats} Sats"
|
||||
)
|
||||
cls.move_state_updated_payout_method(order)
|
||||
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def update_invoice(cls, order, user, invoice, routing_budget_ppm):
|
||||
|
||||
# Empty invoice?
|
||||
if not invoice:
|
||||
order.log(
|
||||
f"Robot({user.robot.id},{user.username}) submitted an empty invoice",
|
||||
level="WARN",
|
||||
)
|
||||
return False, {"bad_invoice": "You submitted an empty invoice"}
|
||||
# only the buyer can post a buyer invoice
|
||||
if not cls.is_buyer(order, user):
|
||||
@ -766,12 +880,12 @@ class Logics:
|
||||
and not order.status == Order.Status.FAI
|
||||
):
|
||||
return False, {
|
||||
"bad_request": "You cannot submit a invoice while bonds are not locked."
|
||||
"bad_request": "You cannot submit an invoice while bonds are not locked."
|
||||
}
|
||||
if order.status == Order.Status.FAI:
|
||||
if order.payout.status != LNPayment.Status.EXPIRE:
|
||||
return False, {
|
||||
"bad_request": "You cannot submit an invoice only after expiration or 3 failed attempts"
|
||||
"bad_request": "You can only submit an invoice after expiration or 3 failed attempts"
|
||||
}
|
||||
|
||||
# cancel onchain_payout if existing
|
||||
@ -811,6 +925,10 @@ class Logics:
|
||||
order.is_swap = False
|
||||
order.save(update_fields=["payout", "is_swap"])
|
||||
|
||||
order.log(
|
||||
f"Robot({user.robot.id},{user.username}) added the invoice LNPayment({order.payout.payment_hash},{order.payout.payment_hash}) as payout method. Amount to be sent is {order.payout.num_satoshis} Sats, routing budget is {order.payout.routing_budget_sats} Sats ({order.payout.routing_budget_ppm}ppm)"
|
||||
)
|
||||
|
||||
cls.move_state_updated_payout_method(order)
|
||||
|
||||
return True, None
|
||||
@ -819,7 +937,7 @@ class Logics:
|
||||
def move_state_updated_payout_method(cls, order):
|
||||
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
|
||||
if order.status == Order.Status.WFI:
|
||||
order.status = Order.Status.CHA
|
||||
order.update_status(Order.Status.CHA)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.CHA)
|
||||
)
|
||||
@ -829,10 +947,11 @@ class Logics:
|
||||
elif order.status == Order.Status.WF2:
|
||||
# If the escrow does not exist, or is not locked move to WFE.
|
||||
if order.trade_escrow is None:
|
||||
order.status = Order.Status.WFE
|
||||
order.update_status(Order.Status.WFE)
|
||||
|
||||
# If the escrow is locked move to Chat.
|
||||
elif order.trade_escrow.status == LNPayment.Status.LOCKED:
|
||||
order.status = Order.Status.CHA
|
||||
order.update_status(Order.Status.CHA)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.CHA)
|
||||
)
|
||||
@ -840,17 +959,17 @@ class Logics:
|
||||
order_id=order.id, message="fiat_exchange_starts"
|
||||
)
|
||||
else:
|
||||
order.status = Order.Status.WFE
|
||||
order.update_status(Order.Status.WFE)
|
||||
|
||||
# If the order status is 'Failed Routing'. Retry payment.
|
||||
elif order.status == Order.Status.FAI:
|
||||
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
|
||||
order.status = Order.Status.PAY
|
||||
order.update_status(Order.Status.PAY)
|
||||
order.payout.status = LNPayment.Status.FLIGHT
|
||||
order.payout.routing_attempts = 0
|
||||
order.payout.save(update_fields=["status", "routing_attempts"])
|
||||
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
return True
|
||||
|
||||
def is_penalized(user):
|
||||
@ -866,7 +985,6 @@ class Logics:
|
||||
|
||||
@classmethod
|
||||
def cancel_order(cls, order, user, state=None):
|
||||
|
||||
# Do not change order status if an is in order
|
||||
# any of these status
|
||||
do_not_cancel = [
|
||||
@ -889,13 +1007,16 @@ class Logics:
|
||||
# status becomes "cancelled"
|
||||
if order.status == Order.Status.WFB and order.maker == user:
|
||||
cls.cancel_bond(order.maker_bond)
|
||||
order.status = Order.Status.UCA
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.UCA)
|
||||
|
||||
order.log("Order expired while waiting for maker bond")
|
||||
order.log("Maker bond was cancelled")
|
||||
|
||||
return True, None
|
||||
|
||||
# 2.a) When maker cancels after bond
|
||||
#
|
||||
# The order dissapears from book and goes to cancelled. If strict, maker is charged the bond
|
||||
# The order disapears from book and goes to cancelled. If strict, maker is charged the bond
|
||||
# to prevent DDOS on the LN node and order book. If not strict, maker is returned
|
||||
# the bond (more user friendly).
|
||||
elif (
|
||||
@ -903,11 +1024,14 @@ class Logics:
|
||||
):
|
||||
# Return the maker bond (Maker gets returned the bond for cancelling public order)
|
||||
if cls.return_bond(order.maker_bond):
|
||||
order.status = Order.Status.UCA
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.UCA)
|
||||
send_notification.delay(
|
||||
order_id=order.id, message="public_order_cancelled"
|
||||
)
|
||||
|
||||
order.log("Order cancelled by maker while public or paused")
|
||||
order.log("Maker bond was <b>unlocked</b>")
|
||||
|
||||
return True, None
|
||||
|
||||
# 2.b) When maker cancels after bond and before taker bond is locked
|
||||
@ -918,11 +1042,15 @@ class Logics:
|
||||
# Return the maker bond (Maker gets returned the bond for cancelling public order)
|
||||
if cls.return_bond(order.maker_bond):
|
||||
cls.cancel_bond(order.taker_bond)
|
||||
order.status = Order.Status.UCA
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.UCA)
|
||||
send_notification.delay(
|
||||
order_id=order.id, message="public_order_cancelled"
|
||||
)
|
||||
|
||||
order.log("Order cancelled by maker before the taker locked the bond")
|
||||
order.log("Maker bond was <b>unlocked</b>")
|
||||
order.log("Taker bond was <b>cancelled</b>")
|
||||
|
||||
return True, None
|
||||
|
||||
# 3) When taker cancels before bond
|
||||
@ -932,6 +1060,9 @@ class Logics:
|
||||
# adds a timeout penalty
|
||||
cls.cancel_bond(order.taker_bond)
|
||||
cls.kick_taker(order)
|
||||
|
||||
order.log("Taker cancelled before locking the bond")
|
||||
|
||||
return True, None
|
||||
|
||||
# 4) When taker or maker cancel after bond (before escrow)
|
||||
@ -950,12 +1081,17 @@ class Logics:
|
||||
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
|
||||
valid = cls.settle_bond(order.maker_bond)
|
||||
cls.return_bond(order.taker_bond) # returns taker bond
|
||||
cls.cancel_escrow(order)
|
||||
|
||||
if valid:
|
||||
order.status = Order.Status.UCA
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.UCA)
|
||||
# Reward taker with part of the maker bond
|
||||
cls.add_slashed_rewards(order, order.maker_bond, order.taker_bond)
|
||||
|
||||
order.log("Maker cancelled before escrow was locked")
|
||||
order.log("Maker bond was <b>settled</b>")
|
||||
order.log("Taker bond was <b>unlocked</b>")
|
||||
|
||||
return True, None
|
||||
|
||||
# 4.b) When taker cancel after bond (before escrow)
|
||||
@ -973,6 +1109,11 @@ class Logics:
|
||||
send_notification.delay(order_id=order.id, message="order_published")
|
||||
# Reward maker with part of the taker bond
|
||||
cls.add_slashed_rewards(order, taker_bond, order.maker_bond)
|
||||
|
||||
order.log("Taker cancelled before escrow was locked")
|
||||
order.log("Taker bond was <b>settled</b>")
|
||||
order.log("Maker bond was <b>unlocked</b>")
|
||||
|
||||
return True, None
|
||||
|
||||
# 5) When trade collateral has been posted (after escrow)
|
||||
@ -982,29 +1123,43 @@ class Logics:
|
||||
# When the second user asks for cancel. Order is totally cancelled.
|
||||
# Must have a small cost for both parties to prevent node DDOS.
|
||||
elif order.status in [Order.Status.WFI, Order.Status.CHA]:
|
||||
|
||||
# if the maker had asked, and now the taker does: cancel order, return everything
|
||||
if order.maker_asked_cancel and user == order.taker:
|
||||
cls.collaborative_cancel(order)
|
||||
order.log(
|
||||
f"Taker Robot({user.robot.id},{user.username}) accepted the collaborative cancellation"
|
||||
)
|
||||
return True, None
|
||||
|
||||
# if the taker had asked, and now the maker does: cancel order, return everything
|
||||
elif order.taker_asked_cancel and user == order.maker:
|
||||
cls.collaborative_cancel(order)
|
||||
order.log(
|
||||
f"Maker Robot({user.robot.id},{user.username}) accepted the collaborative cancellation"
|
||||
)
|
||||
return True, None
|
||||
|
||||
# Otherwise just make true the asked for cancel flags
|
||||
elif user == order.taker:
|
||||
order.taker_asked_cancel = True
|
||||
order.save(update_fields=["taker_asked_cancel"])
|
||||
order.log(
|
||||
f"Taker Robot({user.robot.id},{user.username}) asked for collaborative cancellation"
|
||||
)
|
||||
return True, None
|
||||
|
||||
elif user == order.maker:
|
||||
order.maker_asked_cancel = True
|
||||
order.save(update_fields=["maker_asked_cancel"])
|
||||
order.log(
|
||||
f"Maker Robot({user.robot.id},{user.username}) asked for collaborative cancellation"
|
||||
)
|
||||
return True, None
|
||||
|
||||
else:
|
||||
order.log(
|
||||
f"Cancel request was sent by Robot({user.robot.id},{user.username}) on an invalid status {order.status}: <i>{Order.Status(order.status).label}</i>"
|
||||
)
|
||||
return False, {"bad_request": "You cannot cancel this order"}
|
||||
|
||||
@classmethod
|
||||
@ -1016,9 +1171,14 @@ class Logics:
|
||||
cls.return_bond(order.maker_bond)
|
||||
cls.return_bond(order.taker_bond)
|
||||
cls.return_escrow(order)
|
||||
order.status = Order.Status.CCA
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.CCA)
|
||||
send_notification.delay(order_id=order.id, message="collaborative_cancelled")
|
||||
|
||||
order.log("Order was collaboratively cancelled")
|
||||
order.log("Maker bond was <b>unlocked</b>")
|
||||
order.log("Taker bond was <b>unlocked</b>")
|
||||
order.log("Trade escrow was <b>unlocked</b>")
|
||||
|
||||
return
|
||||
|
||||
@classmethod
|
||||
@ -1041,7 +1201,7 @@ class Logics:
|
||||
|
||||
order.save() # update all fields
|
||||
|
||||
# send_notification.delay(order_id=order.id,'order_published') # too spammy
|
||||
order.log(f"Order({order.id},{str(order)}) is public in the order book")
|
||||
return
|
||||
|
||||
def compute_cltv_expiry_blocks(order, invoice_concept):
|
||||
@ -1131,6 +1291,11 @@ class Logics:
|
||||
)
|
||||
|
||||
order.save(update_fields=["last_satoshis", "last_satoshis_time", "maker_bond"])
|
||||
|
||||
order.log(
|
||||
f"Maker bond LNPayment({order.maker_bond.payment_hash},{str(order.maker_bond)}) was created"
|
||||
)
|
||||
|
||||
return True, {
|
||||
"bond_invoice": hold_payment["invoice"],
|
||||
"bond_satoshis": bond_satoshis,
|
||||
@ -1152,13 +1317,12 @@ class Logics:
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.WF2)
|
||||
)
|
||||
order.status = Order.Status.WF2
|
||||
order.update_status(Order.Status.WF2)
|
||||
order.save(
|
||||
update_fields=[
|
||||
"last_satoshis",
|
||||
"last_satoshis_time",
|
||||
"expires_at",
|
||||
"status",
|
||||
]
|
||||
)
|
||||
|
||||
@ -1170,15 +1334,20 @@ class Logics:
|
||||
|
||||
# Log a market tick
|
||||
try:
|
||||
MarketTick.log_a_tick(order)
|
||||
market_tick = MarketTick.log_a_tick(order)
|
||||
order.log(
|
||||
f"New Market Tick logged as MarketTick({market_tick.id},{market_tick})"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
send_notification.delay(order_id=order.id, message="order_taken_confirmed")
|
||||
order.log(
|
||||
f"<b>Contract formalized.</b> Maker: Robot({order.maker.robot},{order.maker.username}). Taker: Robot({order.taker.robot},{order.taker.username}). API median price {order.currency.exchange_rate} {Currency.currency_choices(order.currency.currency).label}/BTC. Premium is {order.premium}. Contract size {order.last_satoshis} Sats"
|
||||
)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def gen_taker_hold_invoice(cls, order, user):
|
||||
|
||||
# Do not gen and kick out the taker if order is older than expiry time
|
||||
if order.expires_at < timezone.now():
|
||||
cls.order_expires(order)
|
||||
@ -1251,6 +1420,11 @@ class Logics:
|
||||
"expires_at",
|
||||
]
|
||||
)
|
||||
|
||||
order.log(
|
||||
f"Taker bond invoice LNPayment({hold_payment['payment_hash']},{str(order.taker_bond)}) was created"
|
||||
)
|
||||
|
||||
return True, {
|
||||
"bond_invoice": hold_payment["invoice"],
|
||||
"bond_satoshis": bond_satoshis,
|
||||
@ -1260,20 +1434,18 @@ class Logics:
|
||||
"""Moves the order forward"""
|
||||
# If status is 'Waiting for both' move to Waiting for invoice
|
||||
if order.status == Order.Status.WF2:
|
||||
order.status = Order.Status.WFI
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.WFI)
|
||||
# If status is 'Waiting for invoice' move to Chat
|
||||
elif order.status == Order.Status.WFE:
|
||||
order.status = Order.Status.CHA
|
||||
order.update_status(Order.Status.CHA)
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.CHA)
|
||||
)
|
||||
order.save(update_fields=["status", "expires_at"])
|
||||
order.save(update_fields=["expires_at"])
|
||||
send_notification.delay(order_id=order.id, message="fiat_exchange_starts")
|
||||
|
||||
@classmethod
|
||||
def gen_escrow_hold_invoice(cls, order, user):
|
||||
|
||||
# Do not generate if escrow deposit time has expired
|
||||
if order.expires_at < timezone.now():
|
||||
cls.order_expires(order)
|
||||
@ -1292,6 +1464,8 @@ class Logics:
|
||||
escrow_satoshis = cls.escrow_amount(order, user)[1][
|
||||
"escrow_amount"
|
||||
] # Amount was fixed when taker bond was locked, fee applied here
|
||||
order.log(f"Escrow invoice amount is calculated as {escrow_satoshis} Sats")
|
||||
|
||||
if user.robot.wants_stealth:
|
||||
description = f"This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. Payment reference: {order.reference}"
|
||||
else:
|
||||
@ -1334,6 +1508,11 @@ class Logics:
|
||||
)
|
||||
|
||||
order.save(update_fields=["trade_escrow"])
|
||||
|
||||
order.log(
|
||||
f"Trade escrow invoice LNPayment({hold_payment['payment_hash']},{str(order.trade_escrow)}) was created"
|
||||
)
|
||||
|
||||
return True, {
|
||||
"escrow_invoice": hold_payment["invoice"],
|
||||
"escrow_satoshis": escrow_satoshis,
|
||||
@ -1344,6 +1523,7 @@ class Logics:
|
||||
if LNNode.settle_hold_invoice(order.trade_escrow.preimage):
|
||||
order.trade_escrow.status = LNPayment.Status.SETLED
|
||||
order.trade_escrow.save(update_fields=["status"])
|
||||
order.log("Trade escrow was <b>settled</b>")
|
||||
return True
|
||||
|
||||
def settle_bond(bond):
|
||||
@ -1358,6 +1538,7 @@ class Logics:
|
||||
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
|
||||
order.trade_escrow.status = LNPayment.Status.RETNED
|
||||
order.trade_escrow.save(update_fields=["status"])
|
||||
order.log("Trade escrow was <b>unlocked</b>")
|
||||
return True
|
||||
|
||||
def cancel_escrow(order):
|
||||
@ -1366,6 +1547,7 @@ class Logics:
|
||||
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
|
||||
order.trade_escrow.status = LNPayment.Status.CANCEL
|
||||
order.trade_escrow.save(update_fields=["status"])
|
||||
order.log("Trade escrow was <b>cancelled</b>")
|
||||
return True
|
||||
|
||||
def return_bond(bond):
|
||||
@ -1391,6 +1573,11 @@ class Logics:
|
||||
if order.payout_tx:
|
||||
order.payout_tx.status = OnchainPayment.Status.CANCE
|
||||
order.payout_tx.save(update_fields=["status"])
|
||||
|
||||
order.log(
|
||||
f"Onchain payment OnchainPayment({order.payout_tx.id},{str(order.payout_tx)}) was <b>cancelled</b>"
|
||||
)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@ -1423,11 +1610,12 @@ class Logics:
|
||||
order.payout.status = LNPayment.Status.FLIGHT
|
||||
order.payout.save(update_fields=["status"])
|
||||
|
||||
order.status = Order.Status.PAY
|
||||
order.update_status(Order.Status.PAY)
|
||||
order.contract_finalization_time = timezone.now()
|
||||
order.save(update_fields=["status", "contract_finalization_time"])
|
||||
order.save(update_fields=["contract_finalization_time"])
|
||||
|
||||
send_notification.delay(order_id=order.id, message="trade_successful")
|
||||
order.log("<b>Paying buyer invoice</b>")
|
||||
return True
|
||||
|
||||
# Pay onchain to address
|
||||
@ -1439,26 +1627,29 @@ class Logics:
|
||||
order.payout_tx.status = OnchainPayment.Status.QUEUE
|
||||
order.payout_tx.save(update_fields=["status"])
|
||||
|
||||
order.status = Order.Status.SUC
|
||||
order.update_status(Order.Status.SUC)
|
||||
order.contract_finalization_time = timezone.now()
|
||||
order.save(update_fields=["status", "contract_finalization_time"])
|
||||
order.save(update_fields=["contract_finalization_time"])
|
||||
|
||||
send_notification.delay(order_id=order.id, message="trade_successful")
|
||||
order.log("<b>Paying buyer onchain address</b>")
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def confirm_fiat(cls, order, user):
|
||||
"""If Order is in the CHAT states:
|
||||
If user is buyer: fiat_sent goes to true.
|
||||
If User is seller and fiat_sent is true: settle the escrow and pay buyer invoice!"""
|
||||
If User is seller and fiat_sent is true: settle the escrow and pay buyer invoice!
|
||||
"""
|
||||
|
||||
if order.status == Order.Status.CHA or order.status == Order.Status.FSE:
|
||||
|
||||
# If buyer, settle escrow and mark fiat sent
|
||||
# If buyer mark fiat sent
|
||||
if cls.is_buyer(order, user):
|
||||
order.status = Order.Status.FSE
|
||||
order.update_status(Order.Status.FSE)
|
||||
order.is_fiat_sent = True
|
||||
order.save(update_fields=["status", "is_fiat_sent"])
|
||||
order.save(update_fields=["is_fiat_sent"])
|
||||
|
||||
order.log("Buyer confirmed 'fiat sent'")
|
||||
|
||||
# If seller and fiat was sent, SETTLE ESCROW AND PAY BUYER INVOICE
|
||||
elif cls.is_seller(order, user):
|
||||
@ -1488,6 +1679,8 @@ class Logics:
|
||||
# RETURN THE BONDS
|
||||
cls.return_bond(order.taker_bond)
|
||||
cls.return_bond(order.maker_bond)
|
||||
order.log("Taker bond was <b>unlocked</b>")
|
||||
order.log("Maker bond was <b>unlocked</b>")
|
||||
# !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
||||
cls.pay_buyer(order)
|
||||
|
||||
@ -1517,10 +1710,14 @@ class Logics:
|
||||
return False, {
|
||||
"bad_request": "Only orders in Chat and with fiat sent confirmed can be reverted."
|
||||
}
|
||||
order.status = Order.Status.CHA
|
||||
order.update_status(Order.Status.CHA)
|
||||
order.is_fiat_sent = False
|
||||
order.reverted_fiat_sent = True
|
||||
order.save(update_fields=["status", "is_fiat_sent", "reverted_fiat_sent"])
|
||||
order.save(update_fields=["is_fiat_sent", "reverted_fiat_sent"])
|
||||
|
||||
order.log(
|
||||
f"Buyer Robot({user.robot.id},{user.username}) reverted the confirmation of 'fiat sent'"
|
||||
)
|
||||
|
||||
return True, None
|
||||
|
||||
@ -1531,14 +1728,23 @@ class Logics:
|
||||
}
|
||||
else:
|
||||
if order.status == Order.Status.PUB:
|
||||
order.status = Order.Status.PAU
|
||||
order.update_status(Order.Status.PAU)
|
||||
order.log(
|
||||
f"Robot({user.robot.id},{user.username}) paused the public order"
|
||||
)
|
||||
elif order.status == Order.Status.PAU:
|
||||
order.status = Order.Status.PUB
|
||||
order.update_status(Order.Status.PUB)
|
||||
order.log(
|
||||
f"Robot({user.robot.id},{user.username}) made public the paused order"
|
||||
)
|
||||
else:
|
||||
order.log(
|
||||
f"Robot({user.robot.id},{user.username}) tried to pause/unpause an order that was not public or paused",
|
||||
level="WARN",
|
||||
)
|
||||
return False, {
|
||||
"bad_request": "You can only pause/unpause an order that is either public or paused"
|
||||
}
|
||||
order.save(update_fields=["status"])
|
||||
|
||||
return True, None
|
||||
|
||||
@ -1584,12 +1790,13 @@ class Logics:
|
||||
order.proceeds += new_proceeds
|
||||
order.save(update_fields=["proceeds"])
|
||||
send_devfund_donation.delay(order.id, new_proceeds, "slashed bond")
|
||||
|
||||
order.log(
|
||||
f"Robot({rewarded_robot.id},{rewarded_robot.user.username}) was rewarded {reward} Sats. Robot({slashed_robot.id},{slashed_robot.user.username}) was returned {slashed_return} Sats)"
|
||||
)
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def withdraw_rewards(cls, user, invoice):
|
||||
|
||||
# only a user with positive withdraw balance can use this
|
||||
|
||||
if user.robot.earned_rewards < 1:
|
||||
@ -1666,6 +1873,11 @@ class Logics:
|
||||
|
||||
order.proceeds += new_proceeds
|
||||
order.save(update_fields=["proceeds"])
|
||||
|
||||
order.log(
|
||||
f"Order({order.id},{str(order)}) proceedings are incremented by {new_proceeds} Sats, totalling {order.proceeds} Sats"
|
||||
)
|
||||
|
||||
send_devfund_donation.delay(order.id, new_proceeds, "successful order")
|
||||
|
||||
@classmethod
|
||||
@ -1681,7 +1893,6 @@ class Logics:
|
||||
|
||||
users = {"taker": order.taker, "maker": order.maker}
|
||||
for order_user in users:
|
||||
|
||||
summary = {}
|
||||
summary["trade_fee_percent"] = (
|
||||
FEE * MAKER_FEE_SPLIT
|
||||
|
@ -58,8 +58,7 @@ class Command(BaseCommand):
|
||||
|
||||
if "unable to locate invoice" in str(e):
|
||||
self.stdout.write(str(e))
|
||||
order.status = Order.Status.EXP
|
||||
order.save(update_fields=["status"])
|
||||
order.update_status(Order.Status.EXP)
|
||||
debug["expired_orders"].append({idx: context})
|
||||
|
||||
if debug["num_expired_orders"] > 0:
|
||||
|
@ -22,7 +22,6 @@ def is_same_status(a: LNPayment.Status, b: LNPayment.Status) -> bool:
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = "Follows all active hold invoices, sends out payments"
|
||||
rest = 5 # seconds between consecutive checks for invoice updates
|
||||
|
||||
@ -188,7 +187,6 @@ class Command(BaseCommand):
|
||||
follow_send_payment.delay(lnpayment.payment_hash)
|
||||
|
||||
def send_onchain_payments(self):
|
||||
|
||||
queryset = OnchainPayment.objects.filter(
|
||||
status=OnchainPayment.Status.QUEUE,
|
||||
broadcasted=False,
|
||||
@ -229,6 +227,7 @@ class Command(BaseCommand):
|
||||
try:
|
||||
# It is a maker bond => Publish order.
|
||||
if hasattr(lnpayment, "order_made"):
|
||||
lnpayment.order_made.log("Maker bond <b>locked</b>")
|
||||
Logics.publish_order(lnpayment.order_made)
|
||||
send_notification.delay(
|
||||
order_id=lnpayment.order_made.id, message="order_published"
|
||||
@ -238,11 +237,13 @@ class Command(BaseCommand):
|
||||
# It is a taker bond => close contract.
|
||||
elif hasattr(lnpayment, "order_taken"):
|
||||
if lnpayment.order_taken.status == Order.Status.TAK:
|
||||
lnpayment.order_taken.log("Taker bond <b>locked</b>")
|
||||
Logics.finalize_contract(lnpayment.order_taken)
|
||||
return
|
||||
|
||||
# It is a trade escrow => move foward order status.
|
||||
elif hasattr(lnpayment, "order_escrow"):
|
||||
lnpayment.order_escrow.log("Trade escrow <b>locked</b>")
|
||||
Logics.trade_escrow_received(lnpayment.order_escrow)
|
||||
return
|
||||
|
||||
|
19
api/migrations/0041_order_logs.py
Normal file
19
api/migrations/0041_order_logs.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.3 on 2023-08-03 09:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0040_robot_hash_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="logs",
|
||||
field=models.TextField(
|
||||
blank=True, default=None, editable=False, max_length=80000, null=True
|
||||
),
|
||||
),
|
||||
]
|
@ -70,10 +70,12 @@ class MarketTick(models.Model):
|
||||
market_exchange_rate = float(order.currency.exchange_rate)
|
||||
premium = 100 * (price / market_exchange_rate - 1)
|
||||
|
||||
MarketTick.objects.create(
|
||||
market_tick = MarketTick.objects.create(
|
||||
price=price, volume=volume, premium=premium, currency=order.currency
|
||||
)
|
||||
|
||||
return market_tick
|
||||
|
||||
def __str__(self):
|
||||
return f"Tick: {str(self.id)[:8]}"
|
||||
|
||||
|
@ -235,6 +235,14 @@ class Order(models.Model):
|
||||
maker_platform_rated = models.BooleanField(default=False, null=False)
|
||||
taker_platform_rated = models.BooleanField(default=False, null=False)
|
||||
|
||||
logs = models.TextField(
|
||||
max_length=80_000,
|
||||
null=True,
|
||||
default="<thead><tr><b><th>Timestamp</th><th>Level</th><th>Event</th></b></tr></thead>",
|
||||
blank=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.has_range and self.amount is None:
|
||||
amt = str(float(self.min_amount)) + "-" + str(float(self.max_amount))
|
||||
@ -243,7 +251,6 @@ class Order(models.Model):
|
||||
return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}"
|
||||
|
||||
def t_to_expire(self, status):
|
||||
|
||||
t_to_expire = {
|
||||
0: config(
|
||||
"EXP_MAKER_BOND_INVOICE", cast=int, default=300
|
||||
@ -274,6 +281,32 @@ class Order(models.Model):
|
||||
|
||||
return t_to_expire[status]
|
||||
|
||||
def log(self, event="empty event", level="INFO"):
|
||||
"""
|
||||
log() adds a new line to the Order.log field. We wrap it all in a
|
||||
try/catch block since this function is called inside the main request->response
|
||||
pipe and any error here would lead to a 500 response.
|
||||
"""
|
||||
try:
|
||||
timestamp = timezone.now().replace(microsecond=0).isoformat()
|
||||
level_in_tag = "" if level == "INFO" else "<b>"
|
||||
level_out_tag = "" if level == "INFO" else "</b>"
|
||||
self.logs = (
|
||||
self.logs
|
||||
+ f"<tr><td>{timestamp}</td><td>{level_in_tag}{level}{level_out_tag}</td><td>{event}</td></tr>"
|
||||
)
|
||||
self.save(update_fields=["logs"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def update_status(self, new_status):
|
||||
old_status = self.status
|
||||
self.status = new_status
|
||||
self.save(update_fields=["status"])
|
||||
self.log(
|
||||
f"Order state went from {old_status}: <i>{Order.Status(old_status).label}</i> to {new_status}: <i>{Order.Status(new_status).label}</i>"
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Order)
|
||||
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
|
||||
|
@ -123,7 +123,7 @@ def send_devfund_donation(order_id, proceeds, reason):
|
||||
if not valid:
|
||||
return False
|
||||
|
||||
LNPayment.objects.create(
|
||||
lnpayment = LNPayment.objects.create(
|
||||
concept=LNPayment.Concepts.DEVDONAT,
|
||||
type=LNPayment.Types.KEYS,
|
||||
sender=User.objects.get(
|
||||
@ -137,6 +137,9 @@ def send_devfund_donation(order_id, proceeds, reason):
|
||||
**keysend_payment,
|
||||
)
|
||||
|
||||
order.log(
|
||||
f"Development fund donation LNPayment({lnpayment.payment_hash},{str(lnpayment)}) was made via keysend for {num_satoshis} Sats"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@ -209,7 +212,6 @@ def payments_cleansing():
|
||||
soft_time_limit=115,
|
||||
)
|
||||
def cache_market():
|
||||
|
||||
import math
|
||||
|
||||
from django.utils import timezone
|
||||
@ -229,7 +231,6 @@ def cache_market():
|
||||
for i in range(
|
||||
len(Currency.currency_dict.values())
|
||||
): # currencies are indexed starting at 1 (USD)
|
||||
|
||||
rate = exchange_rates[i]
|
||||
results[i] = {currency_codes[i], rate}
|
||||
|
||||
@ -258,7 +259,6 @@ def cache_market():
|
||||
|
||||
@shared_task(name="send_notification", ignore_result=True, time_limit=120)
|
||||
def send_notification(order_id=None, chat_message_id=None, message=None):
|
||||
|
||||
if order_id:
|
||||
from api.models import Order
|
||||
|
||||
|
20
api/utils.py
20
api/utils.py
@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
import gnupg
|
||||
import numpy as np
|
||||
@ -377,3 +378,22 @@ def is_valid_token(token: str) -> bool:
|
||||
|
||||
charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~"'
|
||||
return all(c in charset for c in token)
|
||||
|
||||
|
||||
def objects_to_hyperlinks(logs: str) -> str:
|
||||
"""
|
||||
Parses strings that have Object(ID,NAME) that match API models.
|
||||
For example Robot(ID,NAME) will be parsed into
|
||||
<b><a href="/coordinator/api/robot/ID/change}">NAME</a></b>
|
||||
|
||||
Used to format pretty logs for the Order admin panel.
|
||||
"""
|
||||
objects = ["LNPayment", "Robot", "Order", "OnchainPayment", "MarketTick"]
|
||||
for obj in objects:
|
||||
logs = re.sub(
|
||||
rf"{obj}\(([0-9a-fA-F\-A-F]+),\s*([^)]+)\)",
|
||||
lambda m: f'<b><a href="/coordinator/api/{obj.lower()}/{m.group(1)}">{m.group(2)}</a></b>',
|
||||
logs,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return logs
|
||||
|
@ -183,6 +183,9 @@ class MakerView(CreateAPIView):
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
order.save()
|
||||
order.log(
|
||||
f"Order({order.id},{order}) created by Robot({request.user.robot.id},{request.user})"
|
||||
)
|
||||
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user