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:
Reckless_Satoshi 2023-08-06 17:48:20 +00:00 committed by GitHub
parent 9777a4987f
commit 4383d14f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 455 additions and 119 deletions

View File

@ -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)

View File

@ -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",

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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

View 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
),
),
]

View File

@ -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]}"

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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)