Add password to Orders

This commit is contained in:
koalasat
2025-06-18 15:15:31 +02:00
parent 569ad3f774
commit ef5257ef33
8 changed files with 151 additions and 10 deletions

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.9 on 2025-06-18 12:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0053_alter_currency_currency'),
]
operations = [
migrations.AddField(
model_name='order',
name='password',
field=models.TextField(blank=True, default=None, max_length=2000, null=True),
),
]

View File

@ -165,6 +165,14 @@ class Order(models.Model):
blank=True,
)
# optionally makers can set a password for the order to be taken
password = models.TextField(
max_length=2000,
null=True,
default=None,
blank=True,
)
# how many sats at creation and at last check (relevant for marked to market)
t0_satoshis = models.PositiveBigIntegerField(
null=True,

View File

@ -15,6 +15,10 @@ class Nostr:
async def send_order_event(self, order):
"""Creates the event and sends it to the coordinator relay"""
# Publish only public orders
if order.password is not None:
return
if config("NOSTR_NSEC", cast=str, default="") == "":
return

View File

@ -594,10 +594,18 @@ class MakeOrderSerializer(serializers.ModelSerializer):
"bond_size",
"latitude",
"longitude",
"password",
)
class UpdateOrderSerializer(serializers.Serializer):
password = serializers.CharField(
max_length=2000,
allow_null=True,
allow_blank=True,
default=None,
help_text="In case the order is password protected",
)
invoice = serializers.CharField(
max_length=15000,
allow_null=True,

View File

@ -119,6 +119,7 @@ class MakerView(CreateAPIView):
bond_size = serializer.data.get("bond_size")
latitude = serializer.data.get("latitude")
longitude = serializer.data.get("longitude")
password = serializer.data.get("password")
# Optional params
if public_duration is None:
@ -173,6 +174,7 @@ class MakerView(CreateAPIView):
bond_size=bond_size,
latitude=latitude,
longitude=longitude,
password=password,
)
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
@ -230,6 +232,23 @@ class OrderView(viewsets.ViewSet):
# This is our order.
order = order[0]
data = ListOrderSerializer(order).data
take_order = TakeOrder.objects.filter(
taker=request.user, order=order, expires_at__gt=timezone.now()
)
# Add booleans if user is maker, taker, partipant, buyer or seller
data["is_maker"] = order.maker == request.user
data["is_taker"] = order.taker == request.user or take_order.exists()
data["is_participant"] = data["is_maker"] or data["is_taker"]
# 1) If order has a password
if not data["is_participant"] and order.password is not None:
return Response(
{"bad_request": "This order is password protected"},
status.HTTP_403_FORBIDDEN,
)
# 2) If order has been cancelled
if order.status == Order.Status.UCA:
return Response(
@ -242,7 +261,6 @@ class OrderView(viewsets.ViewSet):
status.HTTP_400_BAD_REQUEST,
)
data = ListOrderSerializer(order).data
data["total_secs_exp"] = order.t_to_expire(order.status)
# if user is under a limit (penalty), inform him.
@ -250,14 +268,6 @@ class OrderView(viewsets.ViewSet):
if is_penalized:
data["penalty"] = request.user.robot.penalty_expiration
# Add booleans if user is maker, taker, partipant, buyer or seller
take_order = TakeOrder.objects.filter(
taker=request.user, order=order, expires_at__gt=timezone.now()
)
data["is_maker"] = order.maker == request.user
data["is_taker"] = order.taker == request.user or take_order.exists()
data["is_participant"] = data["is_maker"] or data["is_taker"]
# 3.a) If not a participant and order is not public, forbid.
if (
order.maker != request.user
@ -550,6 +560,7 @@ class OrderView(viewsets.ViewSet):
statement = serializer.data.get("statement")
rating = serializer.data.get("rating")
cancel_status = serializer.data.get("cancel_status")
password = serializer.data.get("password")
# 1) If action is take, it is a taker request!
if action == "take":
@ -558,6 +569,12 @@ class OrderView(viewsets.ViewSet):
if not valid:
return Response(context, status=status.HTTP_409_CONFLICT)
if order.password is not None and order.password != password:
return Response(
{"bad_request": "Wrong password"},
status=status.HTTP_403_FORBIDDEN,
)
# For order with amount range, set the amount now.
if order.has_range:
amount = float(serializer.data.get("amount"))
@ -734,7 +751,7 @@ class BookView(ListAPIView):
currency = request.GET.get("currency", 0)
type = request.GET.get("type", 2)
queryset = Order.objects.filter(status=Order.Status.PUB)
queryset = Order.objects.filter(status=Order.Status.PUB, password=None)
# Currency 0 and type 2 are special cases treated as "ANY". (These are not really possible choices)
if int(currency) == 0 and int(type) != 2:

View File

@ -1328,6 +1328,10 @@ components:
format: decimal
pattern: ^-?\d{0,3}(?:\.\d{0,6})?$
nullable: true
password:
type: string
nullable: true
maxLength: 2000
required:
- currency
- type
@ -1988,6 +1992,11 @@ components:
UpdateOrder:
type: object
properties:
password:
type: string
nullable: true
description: In case the order is password protected
maxLength: 2000
invoice:
type: string
nullable: true

View File

@ -479,6 +479,60 @@ class TradeTest(BaseAPITestCase):
# Cancel order to avoid leaving pending HTLCs after a successful test
trade.cancel_order()
def test_make_and_take_password_order(self):
"""
Tests a trade with a password from order creation to taken.
"""
password = "1234567"
password_maker_form = maker_form_buy_with_range.copy()
password_maker_form["password"] = password
trade = Trade(
self.client,
# add password to order
maker_form=password_maker_form,
)
trade.publish_order()
data = trade.response.json()
self.assertEqual(trade.response.status_code, 200)
self.assertResponse(trade.response)
# Maker GET
trade.get_order(trade.maker_index)
data = trade.response.json()
self.assertEqual(trade.response.status_code, 200)
# External user GET
trade.get_order(trade.taker_index)
data = trade.response.json()
self.assertEqual(trade.response.status_code, 403)
self.assertEqual(data["bad_request"], "This order is password protected")
# Take with no password
trade.take_order()
data = trade.response.json()
self.assertEqual(trade.response.status_code, 403)
self.assertEqual(data["bad_request"], "Wrong password")
# Take with wrong password
trade.take_password_order("test")
data = trade.response.json()
self.assertEqual(trade.response.status_code, 403)
self.assertEqual(data["bad_request"], "Wrong password")
# Take with right password
trade.take_password_order(password)
data = trade.response.json()
self.assertEqual(trade.response.status_code, 200)
self.assertResponse(trade.response)
self.assertEqual(data["status_message"], Order.Status(Order.Status.PUB).label)
self.assertAlmostEqual(float(data["amount"]), 100)
# Cancel order to avoid leaving pending HTLCs after a successful test
trade.cancel_order()
def test_make_and_take_order_multiple_takers(self):
"""
Tests a trade from order creation to taken.

View File

@ -182,6 +182,21 @@ class Trade:
# Get order
self.get_order()
@patch("api.tasks.send_notification.delay", send_notification)
def publish_password_order(self):
# Maker's first order fetch. Should trigger maker bond hold invoice generation.
self.get_order()
invoice = self.response.json()["bond_invoice"]
# Lock the invoice from the robot's node
pay_invoice("robot", invoice)
# Check for invoice locked (the mocked LND will return ACCEPTED)
self.follow_hold_invoices()
# Get order
self.get_order()
@patch("api.tasks.send_notification.delay", send_notification)
def take_order(self):
path = reverse("order")
@ -190,6 +205,14 @@ class Trade:
body = {"action": "take", "amount": self.take_amount}
self.response = self.client.post(path + params, body, **headers)
@patch("api.tasks.send_notification.delay", send_notification)
def take_password_order(self, password):
path = reverse("order")
params = f"?order_id={self.order_id}"
headers = self.get_robot_auth(self.taker_index)
body = {"action": "take", "amount": self.take_amount, "password": password}
self.response = self.client.post(path + params, body, **headers)
@patch("api.tasks.send_notification.delay", send_notification)
def take_order_third(self):
path = reverse("order")