mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-29 15:23:34 +03:00
Add logics for Invoice update/creation and maker_hodl_invoice
This commit is contained in:
parent
46c129bf80
commit
5505476ea4
@ -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'''
|
||||
|
@ -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
|
||||
|
||||
|
75
api/views.py
75
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):
|
||||
|
Loading…
Reference in New Issue
Block a user