mirror of
https://github.com/RoboSats/robosats.git
synced 2025-02-22 05:09:01 +00: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
|
from django.utils import timezone
|
||||||
|
|
||||||
import random
|
import random
|
||||||
@ -12,9 +13,14 @@ class LNNode():
|
|||||||
Place holder functions to interact with Lightning Node
|
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'''
|
'''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():
|
def validate_hodl_invoice_locked():
|
||||||
'''Generates hodl invoice to publish an order'''
|
'''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.dispatch import receiver
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .lightning import LNNode
|
from .lightning import LNNode
|
||||||
@ -75,10 +78,10 @@ class Order(models.Model):
|
|||||||
ETH = 3, 'ETH'
|
ETH = 3, 'ETH'
|
||||||
|
|
||||||
class Status(models.IntegerChoices):
|
class Status(models.IntegerChoices):
|
||||||
WFB = 0, 'Waiting for bond'
|
WFB = 0, 'Waiting for maker bond'
|
||||||
PUB = 1, 'Published in order book'
|
PUB = 1, 'Public'
|
||||||
DEL = 2, 'Deleted from order book'
|
DEL = 2, 'Deleted'
|
||||||
TAK = 3, 'Taken'
|
TAK = 3, 'Waiting for taker bond' # only needed when taker is a buyer
|
||||||
UCA = 4, 'Unilaterally cancelled'
|
UCA = 4, 'Unilaterally cancelled'
|
||||||
RET = 5, 'Returned to order book' # Probably same as 1 in most cases.
|
RET = 5, 'Returned to order book' # Probably same as 1 in most cases.
|
||||||
WF2 = 6, 'Waiting for trade collateral and buyer invoice'
|
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.
|
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
||||||
is_explicit = models.BooleanField(default=False, null=False)
|
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)
|
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
|
# explicit
|
||||||
satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True)
|
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
|
# 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
|
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():
|
class Logics():
|
||||||
|
|
||||||
|
# escrow_user = User.objects.get(username=ESCROW_USERNAME)
|
||||||
|
|
||||||
def validate_already_maker_or_taker(user):
|
def validate_already_maker_or_taker(user):
|
||||||
'''Checks if the user is already partipant of an order'''
|
'''Checks if the user is already partipant of an order'''
|
||||||
queryset = Order.objects.filter(maker=user)
|
queryset = Order.objects.filter(maker=user)
|
||||||
@ -197,16 +205,30 @@ class Logics():
|
|||||||
is_taker = order.taker == user
|
is_taker = order.taker == user
|
||||||
return (is_maker and order.type == Order.Types.SELL) or (is_taker and order.type == Order.Types.BUY)
|
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
|
@classmethod
|
||||||
def update_invoice(cls, order, user, invoice):
|
def update_invoice(cls, order, user, invoice):
|
||||||
is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(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
|
# only user is the buyer and a valid LN invoice
|
||||||
if cls.is_buyer(order, user) and is_valid_invoice:
|
if cls.is_buyer(order, user) and is_valid_invoice:
|
||||||
order.buyer_invoice, created = LNPayment.objects.update_or_create(
|
order.buyer_invoice, _ = LNPayment.objects.update_or_create(
|
||||||
receiver= user,
|
|
||||||
concept = LNPayment.Concepts.PAYBUYER,
|
concept = LNPayment.Concepts.PAYBUYER,
|
||||||
type = LNPayment.Types.NORM,
|
type = LNPayment.Types.NORM,
|
||||||
sender = User.objects.get(username=ESCROW_USERNAME),
|
sender = User.objects.get(username=ESCROW_USERNAME),
|
||||||
|
receiver= user,
|
||||||
# if there is a LNPayment matching these above, it updates that with defaults below.
|
# if there is a LNPayment matching these above, it updates that with defaults below.
|
||||||
defaults={
|
defaults={
|
||||||
'invoice' : invoice,
|
'invoice' : invoice,
|
||||||
@ -224,3 +246,35 @@ class Logics():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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
|
||||||
|
|
||||||
|
43
api/views.py
43
api/views.py
@ -22,7 +22,7 @@ from datetime import timedelta
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
# .env
|
# .env
|
||||||
expiration_time = 8
|
EXPIRATION_MAKE = 5 # minutes
|
||||||
|
|
||||||
avatar_path = Path('frontend/static/assets/avatars')
|
avatar_path = Path('frontend/static/assets/avatars')
|
||||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
avatar_path.mkdir(parents=True, exist_ok=True)
|
||||||
@ -51,14 +51,14 @@ class OrderMakerView(CreateAPIView):
|
|||||||
# Creates a new order in db
|
# Creates a new order in db
|
||||||
order = Order(
|
order = Order(
|
||||||
type=otype,
|
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,
|
currency=currency,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
payment_method=payment_method,
|
payment_method=payment_method,
|
||||||
premium=premium,
|
premium=premium,
|
||||||
satoshis=satoshis,
|
satoshis=satoshis,
|
||||||
is_explicit=is_explicit,
|
is_explicit=is_explicit,
|
||||||
expires_at= timezone.now()+timedelta(hours=expiration_time),
|
expires_at= timezone.now()+timedelta(minutes=EXPIRATION_MAKE),
|
||||||
maker=request.user)
|
maker=request.user)
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
@ -75,42 +75,49 @@ class OrderView(viewsets.ViewSet):
|
|||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
order_id = request.GET.get(self.lookup_url_kwarg)
|
||||||
|
|
||||||
if order_id != None:
|
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)
|
order = Order.objects.filter(id=order_id)
|
||||||
|
|
||||||
# check if exactly one order is found in the db
|
# check if exactly one order is found in the db
|
||||||
if len(order) == 1 :
|
if len(order) == 1 :
|
||||||
order = order[0]
|
order = order[0]
|
||||||
|
|
||||||
|
# If order expired
|
||||||
|
if order.status == Order.Status.EXP:
|
||||||
|
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
data = ListOrderSerializer(order).data
|
data = ListOrderSerializer(order).data
|
||||||
nickname = request.user.username
|
|
||||||
|
|
||||||
# Add booleans if user is maker, taker, partipant, buyer or seller
|
# Add booleans if user is maker, taker, partipant, buyer or seller
|
||||||
data['is_maker'] = str(order.maker) == nickname
|
data['is_maker'] = order.maker == request.user
|
||||||
data['is_taker'] = str(order.taker) == nickname
|
data['is_taker'] = order.taker == request.user
|
||||||
data['is_participant'] = data['is_maker'] or data['is_taker']
|
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 a participant and order is not public, forbid.
|
||||||
if not data['is_participant'] and order.status != Order.Status.PUB:
|
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)
|
return Response({'bad_request':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
# return nicks too
|
# 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)
|
||||||
|
|
||||||
|
# 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['maker_nick'] = str(order.maker)
|
||||||
data['taker_nick'] = str(order.taker)
|
data['taker_nick'] = str(order.taker)
|
||||||
|
|
||||||
data['status_message'] = Order.Status(order.status).label
|
data['status_message'] = Order.Status(order.status).label
|
||||||
|
|
||||||
if data['is_participant']:
|
# If status is 'waiting for maker bond', reply with a hodl invoice too.
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
if order.status == Order.Status.WFB and data['is_maker']:
|
||||||
else:
|
data['hodl_invoice'] = Logics.gen_maker_hodl_invoice(order, request.user)
|
||||||
# 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)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND)
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
def take_or_update(self, request, format=None):
|
def take_or_update(self, request, format=None):
|
||||||
|
Loading…
Reference in New Issue
Block a user