mirror of
https://github.com/RoboSats/robosats.git
synced 2025-09-13 00:56:22 +00:00
Add password to Orders
This commit is contained in:
18
api/migrations/0054_order_password.py
Normal file
18
api/migrations/0054_order_password.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
37
api/views.py
37
api/views.py
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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")
|
||||
|
Reference in New Issue
Block a user