Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple takers #1782

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/lightning/cln.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hashlib
import os
import random
import secrets
import struct
import time
Expand Down Expand Up @@ -273,7 +274,7 @@ def gen_hold_invoice(
request = hold_pb2.HoldInvoiceRequest(
description=description,
amount_msat=hold_pb2.Amount(msat=num_satoshis * 1_000),
label=f"Order:{order_id}-{lnpayment_concept}-{time}",
label=f"Order:{order_id}-{lnpayment_concept}-{time}--{random.randint(1, 100000)}",
expiry=invoice_expiry,
cltv=cltv_expiry_blocks,
preimage=preimage, # preimage is actually optional in cln, as cln would generate one by default
Expand Down
226 changes: 125 additions & 101 deletions api/logics.py

Large diffs are not rendered by default.

39 changes: 38 additions & 1 deletion api/management/commands/clean_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.utils import timezone

from api.logics import Logics
from api.models import Order
from api.models import Order, TakeOrder


class Command(BaseCommand):
Expand Down Expand Up @@ -44,6 +44,15 @@ def clean_orders(self):
if Logics.order_expires(order): # Order send to expire here
debug["expired_orders"].append({idx: context})

# expire all related take orders
take_orders_queryset = TakeOrder.objects.filter(
order=order, expires_at__gt=timezone.now()
)
for idx, take_order in enumerate(take_orders_queryset):
take_order.expires_at = order.expires_at
take_order.save()
Logics.take_order_expires(take_order)

# It should not happen, but if it cannot locate the hold invoice
# it probably was cancelled by another thread, make it expire anyway.
except Exception as e:
Expand All @@ -59,6 +68,34 @@ def clean_orders(self):
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))

take_orders_queryset = TakeOrder.objects.filter(expires_at__lt=timezone.now())
debug["num_expired_take_orders"] = len(take_orders_queryset)
debug["expired_take_orders"] = []
debug["failed_take_order_expiry"] = []
debug["reason_take_failure"] = []

for idx, take_order in enumerate(take_orders_queryset):
context = str(take_order) + " was expired"
try:
if Logics.take_order_expires(
take_order
): # Take order send to expire here
debug["expired_take_orders"].append({idx: context})

# It should not happen, but if it cannot locate the hold invoice
# it probably was cancelled by another thread, make it expire anyway.
except Exception as e:
debug["failed_take_order_expiry"].append({idx: context})
debug["reason_take_failure"].append({idx: str(e)})

if "unable to locate invoice" in str(e):
self.stdout.write(str(e))
debug["expired_take_orders"].append({idx: context})

if debug["num_expired_take_orders"] > 0:
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))

def handle(self, *args, **options):
"""Never mind database locked error, keep going, print them out.
Not an issue with PostgresQL"""
Expand Down
42 changes: 34 additions & 8 deletions api/management/commands/follow_invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,22 +227,38 @@ def update_order_status(self, lnpayment):
try:
# It is a maker bond => Publish order.
if hasattr(lnpayment, "order_made"):
self.stderr.write("Updating order with new Locked bond from maker")
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"
)
return

# 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 taker bond
elif hasattr(lnpayment, "take_order"):
if lnpayment.take_order.order.status == Order.Status.PUB:
# It there was no other taker already locked => close contract.
self.stderr.write(
"Updating order with new Locked bond from taker"
)
lnpayment.take_order.order.log("Taker bond <b>locked</b>")
Logics.finalize_contract(lnpayment.take_order)
else:
# It there was another taker already locked => cancel bond.
self.stderr.write(
"Expiring take_order because order was already taken"
)
lnpayment.take_order.order.log(
"Another taker bond is already locked, <b>Cancelling</b>"
)
Logics.take_order_expires(lnpayment.take_order)

return

# It is a trade escrow => move foward order status.
elif hasattr(lnpayment, "order_escrow"):
self.stderr.write("Updating order with new Locked escrow")
lnpayment.order_escrow.log("Trade escrow <b>locked</b>")
Logics.trade_escrow_received(lnpayment.order_escrow)
return
Expand All @@ -269,17 +285,27 @@ def update_order_status(self, lnpayment):
# Testing needed for end of time trades!
elif lnpayment.status == LNPayment.Status.CANCEL:
if hasattr(lnpayment, "order_made"):
self.stderr.write("Expiting order with cancelled payent from maker")
Logics.order_expires(lnpayment.order_made)
return

elif hasattr(lnpayment, "order_taken"):
Logics.order_expires(lnpayment.order_taken)
elif hasattr(lnpayment, "take_order"):
self.stderr.write(
"Expiting order and take orders with cancelled payent from taker"
)
Logics.take_order_expires(lnpayment.take_order)
return

elif hasattr(lnpayment, "order_escrow"):
self.stderr.write("Expiting order with cancelled escrow")
Logics.order_expires(lnpayment.order_escrow)
return

elif hasattr(lnpayment, "order_taken"):
self.stderr.write("Expiting take order with cancelled escrow")
Logics.take_order_expires(lnpayment.order_escrow)
return

# TODO If a lnpayment goes from LOCKED to INVGEN. Totally weird
# halt the order
elif lnpayment.status == LNPayment.Status.INVGEN:
Expand Down
30 changes: 30 additions & 0 deletions api/migrations/0051_takeorder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 5.1.4 on 2025-02-24 13:10

import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0050_alter_order_status'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='TakeOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(blank=True, decimal_places=8, max_digits=18, null=True)),
('expires_at', models.DateTimeField()),
('last_satoshis', models.PositiveBigIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000000)])),
('last_satoshis_time', models.DateTimeField(blank=True, default=None, null=True)),
('order', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order', to='api.order')),
('taker', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pretaker', to=settings.AUTH_USER_MODEL)),
('taker_bond', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.lnpayment')),
],
),
]
2 changes: 2 additions & 0 deletions api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .order import Order
from .robot import Robot
from .notification import Notification
from .take_order import TakeOrder

__all__ = [
"Currency",
Expand All @@ -14,4 +15,5 @@
"Order",
"Robot",
"Notification",
"TakeOrder",
]
51 changes: 51 additions & 0 deletions api/models/take_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.core.validators import MaxValueValidator, MinValueValidator
from django.contrib.auth.models import User
from django.db import models
from django.conf import settings
from django.utils import timezone


class TakeOrder(models.Model):
amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
order = models.ForeignKey(
"api.Order",
related_name="order",
on_delete=models.CASCADE,
null=False,
default=None,
blank=False,
)
taker = models.ForeignKey(
User,
related_name="pretaker",
on_delete=models.CASCADE,
null=False,
default=None,
blank=False,
)
expires_at = models.DateTimeField()
taker_bond = models.OneToOneField(
"api.LNPayment",
related_name="take_order",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
)
last_satoshis = models.PositiveBigIntegerField(
null=True,
default=None,
validators=[MinValueValidator(0), MaxValueValidator(settings.MAX_TRADE * 2)],
blank=True,
)
# timestamp of last_satoshis
last_satoshis_time = models.DateTimeField(null=True, default=None, blank=True)

def cancel(self, cls):
if self.expires_at > timezone.now():
self.expires_at = timezone.now()
self.save(update_fields=["expires_at"])
cls.cancel_bond(self.taker_bond)

def __str__(self):
return f"Order {self.order.id} taken by Robot({self.taker.robot.id},{self.taker.username}) for {self.amount} fiat units"
54 changes: 35 additions & 19 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
OnchainPayment,
Order,
Notification,
TakeOrder,
)
from api.notifications import Notifications
from api.oas_schemas import (
Expand Down Expand Up @@ -243,12 +244,19 @@ def get(self, request, format=None):
data["penalty"] = request.user.robot.penalty_expiration

# Add booleans if user is maker, taker, partipant, buyer or seller
is_pretaker = TakeOrder.objects.filter(
taker=request.user, order=order, expires_at__gt=timezone.now()
).exists()
data["is_maker"] = order.maker == request.user
data["is_taker"] = order.taker == request.user
data["is_taker"] = order.taker == request.user or is_pretaker
data["is_participant"] = data["is_maker"] or data["is_taker"]

# 3.a) If not a participant and order is not public, forbid.
if not data["is_participant"] and order.status != Order.Status.PUB:
if (
order.maker != request.user
and order.taker != request.user
and order.status != Order.Status.PUB
):
return Response(
{"bad_request": "This order is not available"},
status.HTTP_403_FORBIDDEN,
Expand Down Expand Up @@ -355,9 +363,17 @@ def get(self, request, format=None):
else:
return Response(context, status.HTTP_400_BAD_REQUEST)

# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER hold invoice.
elif order.status == Order.Status.TAK and data["is_taker"]:
# 6) If status is 'Public' and user is PRETAKER, reply with a TAKER hold invoice.
elif (
order.status == Order.Status.PUB
and is_pretaker
and order.taker != request.user
):
data["status"] = Order.Status.TAK
data["total_secs_exp"] = order.t_to_expire(Order.Status.TAK)

valid, context = Logics.gen_taker_hold_invoice(order, request.user)

if valid:
data = {**data, **context}
else:
Expand Down Expand Up @@ -547,14 +563,20 @@ def take_update_confirm_dispute_cancel(self, request, format=None):
status.HTTP_400_BAD_REQUEST,
)

# 2) If action is cancel
elif action == "cancel":
valid, context = Logics.cancel_order(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

# Any other action is only allowed if the user is a participant
if not (order.maker == request.user or order.taker == request.user):
elif not (order.maker == request.user or order.taker == request.user):
return Response(
{"bad_request": "You are not a participant in this order"},
status.HTTP_403_FORBIDDEN,
)

# 2) If action is 'update invoice'
# 3) If action is 'update invoice'
elif action == "update_invoice":
# DEPRECATE post v0.5.1.
valid_signature, invoice = verify_signed_message(
Expand All @@ -573,7 +595,7 @@ def take_update_confirm_dispute_cancel(self, request, format=None):
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

# 2.b) If action is 'update address'
# 3.b) If action is 'update address'
elif action == "update_address":
valid_signature, address = verify_signed_message(
request.user.robot.public_key, pgp_address
Expand All @@ -591,25 +613,19 @@ def take_update_confirm_dispute_cancel(self, request, format=None):
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

# 3) If action is cancel
elif action == "cancel":
valid, context = Logics.cancel_order(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

# 4) If action is confirm
# 5) If action is confirm
elif action == "confirm":
valid, context = Logics.confirm_fiat(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

# 4.b) If action is confirm
# 5.b) If action is confirm
elif action == "undo_confirm":
valid, context = Logics.undo_confirm_fiat_sent(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

# 5) If action is dispute
# 6) If action is dispute
elif action == "dispute":
valid, context = Logics.open_dispute(order, request.user)
if not valid:
Expand All @@ -620,18 +636,18 @@ def take_update_confirm_dispute_cancel(self, request, format=None):
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

# 6) If action is rate
# 7) If action is rate
elif action == "rate_user" and rating:
"""No user rating"""
pass

# 7) If action is rate_platform
# 8) If action is rate_platform
elif action == "rate_platform" and rating:
valid, context = Logics.rate_platform(request.user, rating)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)

# 8) If action is rate_platform
# 9) If action is rate_platform
elif action == "pause":
valid, context = Logics.pause_unpause_public_order(order, request.user)
if not valid:
Expand Down
Loading