From 5505476ea4b6376ee6465ca05c54dbb971323f0a Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 6 Jan 2022 05:55:47 -0800 Subject: [PATCH] Add logics for Invoice update/creation and maker_hodl_invoice --- api/lightning.py | 10 +++++-- api/models.py | 70 ++++++++++++++++++++++++++++++++++++++------ api/views.py | 75 ++++++++++++++++++++++++++---------------------- 3 files changed, 111 insertions(+), 44 deletions(-) diff --git a/api/lightning.py b/api/lightning.py index de480279..6f2b9eae 100644 --- a/api/lightning.py +++ b/api/lightning.py @@ -1,3 +1,4 @@ +from datetime import timedelta from django.utils import timezone import random @@ -12,9 +13,14 @@ class LNNode(): Place holder functions to interact with Lightning Node ''' - def gen_hodl_invoice(): + def gen_hodl_invoice(num_satoshis, description): '''Generates hodl invoice to publish an order''' - return ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) + # TODO + invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX + payment_hash = ''.join(random.choices(string.ascii_uppercase + string.digits, k=40)) #FIX + expires_at = timezone.now() + timedelta(hours=8) ##FIX + + return invoice, payment_hash, expires_at def validate_hodl_invoice_locked(): '''Generates hodl invoice to publish an order''' diff --git a/api/models.py b/api/models.py index b5cb5135..44a2f7ab 100644 --- a/api/models.py +++ b/api/models.py @@ -5,6 +5,9 @@ from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from django.utils.html import mark_safe +from datetime import timedelta +from django.utils import timezone + from pathlib import Path from .lightning import LNNode @@ -75,10 +78,10 @@ class Order(models.Model): ETH = 3, 'ETH' class Status(models.IntegerChoices): - WFB = 0, 'Waiting for bond' - PUB = 1, 'Published in order book' - DEL = 2, 'Deleted from order book' - TAK = 3, 'Taken' + WFB = 0, 'Waiting for maker bond' + PUB = 1, 'Public' + DEL = 2, 'Deleted' + TAK = 3, 'Waiting for taker bond' # only needed when taker is a buyer UCA = 4, 'Unilaterally cancelled' RET = 5, 'Returned to order book' # Probably same as 1 in most cases. WF2 = 6, 'Waiting for trade collateral and buyer invoice' @@ -109,11 +112,14 @@ class Order(models.Model): # order pricing method. A explicit amount of sats, or a relative premium above/below market. is_explicit = models.BooleanField(default=False, null=False) - # marked to marked + # marked to market premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True) - t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # explicit satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) + # how many sats at creation and at last check (relevant for marked to market) + t0_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # sats at creation + last_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE*2)], blank=True) # sats last time checked. Weird if 2* trade max... + # order participants maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order @@ -172,6 +178,8 @@ class Profile(models.Model): class Logics(): + # escrow_user = User.objects.get(username=ESCROW_USERNAME) + def validate_already_maker_or_taker(user): '''Checks if the user is already partipant of an order''' queryset = Order.objects.filter(maker=user) @@ -197,16 +205,30 @@ class Logics(): is_taker = order.taker == user return (is_maker and order.type == Order.Types.SELL) or (is_taker and order.type == Order.Types.BUY) + def satoshis_now(order): + ''' checks trade amount in sats ''' + # TODO + # order.last_satoshis = + # order.save() + + return 50000 + + def order_expires(order): + order.status = Order.Status.EXP + order.maker = None + order.taker = None + order.save() + @classmethod def update_invoice(cls, order, user, invoice): is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) # only user is the buyer and a valid LN invoice if cls.is_buyer(order, user) and is_valid_invoice: - order.buyer_invoice, created = LNPayment.objects.update_or_create( - receiver= user, + order.buyer_invoice, _ = LNPayment.objects.update_or_create( concept = LNPayment.Concepts.PAYBUYER, type = LNPayment.Types.NORM, sender = User.objects.get(username=ESCROW_USERNAME), + receiver= user, # if there is a LNPayment matching these above, it updates that with defaults below. defaults={ 'invoice' : invoice, @@ -224,3 +246,35 @@ class Logics(): return True return False + + @classmethod + def gen_maker_hodl_invoice(cls, order, user): + + # Do not and delete if order is more than 5 minutes old + if order.expires_at < timezone.now(): + cls.order_expires(order) + return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + + if order.maker_bond: + return order.maker_bond.invoice + + bond_amount = cls.satoshis_now(order) + description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_amount, description=description) + + order.maker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.MAKEBOND, + type = LNPayment.Types.HODL, + sender = user, + receiver = User.objects.get(username=ESCROW_USERNAME), + invoice = invoice, + status = LNPayment.Status.INVGEN, + num_satoshis = bond_amount, + description = description, + payment_hash = payment_hash, + expires_at = expires_at, + ) + + order.save() + return invoice + diff --git a/api/views.py b/api/views.py index 1a7eaadd..0ea88ee4 100644 --- a/api/views.py +++ b/api/views.py @@ -22,7 +22,7 @@ from datetime import timedelta from django.utils import timezone # .env -expiration_time = 8 +EXPIRATION_MAKE = 5 # minutes avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) @@ -51,14 +51,14 @@ class OrderMakerView(CreateAPIView): # Creates a new order in db order = Order( type=otype, - status=Order.Status.PUB, # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) + status=Order.Status.WFB, currency=currency, amount=amount, payment_method=payment_method, premium=premium, satoshis=satoshis, is_explicit=is_explicit, - expires_at= timezone.now()+timedelta(hours=expiration_time), + expires_at= timezone.now()+timedelta(minutes=EXPIRATION_MAKE), maker=request.user) order.save() @@ -75,42 +75,49 @@ class OrderView(viewsets.ViewSet): def get(self, request, format=None): order_id = request.GET.get(self.lookup_url_kwarg) - if order_id != None: - order = Order.objects.filter(id=order_id) + if order_id == None: + return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) + + order = Order.objects.filter(id=order_id) - # check if exactly one order is found in the db - if len(order) == 1 : - order = order[0] - data = ListOrderSerializer(order).data - nickname = request.user.username + # check if exactly one order is found in the db + if len(order) == 1 : + order = order[0] - # Add booleans if user is maker, taker, partipant, buyer or seller - data['is_maker'] = str(order.maker) == nickname - data['is_taker'] = str(order.taker) == nickname - data['is_participant'] = data['is_maker'] or data['is_taker'] - data['is_buyer'] = (data['is_maker'] and order.type == Order.Types.BUY) or (data['is_taker'] and order.type == Order.Types.SELL) - data['is_seller'] = (data['is_maker'] and order.type == Order.Types.SELL) or (data['is_taker'] and order.type == Order.Types.BUY) - - # If not a participant and order is not public, forbid. - if not data['is_participant'] and order.status != Order.Status.PUB: - return Response({'bad_request':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) + # If order expired + if order.status == Order.Status.EXP: + return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST) - # return nicks too - data['maker_nick'] = str(order.maker) - data['taker_nick'] = str(order.taker) - - data['status_message'] = Order.Status(order.status).label + data = ListOrderSerializer(order).data - if data['is_participant']: - return Response(data, status=status.HTTP_200_OK) - else: - # Non participants should not see the status, who is the taker, etc - for key in ('status','status_message','taker','taker_nick','is_maker','is_taker','is_buyer','is_seller'): - del data[key] - return Response(data, status=status.HTTP_200_OK) + # 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 + data['is_participant'] = data['is_maker'] or data['is_taker'] + + # If not a participant and order is not public, forbid. + if not data['is_participant'] and order.status != Order.Status.PUB: + return Response({'bad_request':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) + + # non participants can view some details, but only if PUB + elif not data['is_participant'] and order.status != Order.Status.PUB: + return Response(data, status=status.HTTP_200_OK) - return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) - return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) + # For participants add position side, nicks and status as message + data['is_buyer'] = Logics.is_buyer(order,request.user) + data['is_seller'] = Logics.is_seller(order,request.user) + data['maker_nick'] = str(order.maker) + data['taker_nick'] = str(order.taker) + data['status_message'] = Order.Status(order.status).label + + # If status is 'waiting for maker bond', reply with a hodl invoice too. + if order.status == Order.Status.WFB and data['is_maker']: + data['hodl_invoice'] = Logics.gen_maker_hodl_invoice(order, request.user) + + return Response(data, status=status.HTTP_200_OK) + + return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) + def take_or_update(self, request, format=None):