Add logics for Invoice update/creation and maker_hodl_invoice

This commit is contained in:
Reckless_Satoshi 2022-01-06 05:55:47 -08:00
parent 46c129bf80
commit 5505476ea4
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
3 changed files with 111 additions and 44 deletions

View File

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

View File

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

View File

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