diff --git a/api/admin.py b/api/admin.py index 15bebbaf..aedb441b 100644 --- a/api/admin.py +++ b/api/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.db import models from django.contrib.auth.models import Group, User from django.contrib.auth.admin import UserAdmin -from .models import Order, Profile +from .models import Order, LNPayment, Profile admin.site.unregister(Group) admin.site.unregister(User) @@ -24,7 +24,13 @@ class EUserAdmin(UserAdmin): @admin.register(Order) class OrderAdmin(admin.ModelAdmin): - list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'invoice') + list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'buyer_invoice','maker_bond','taker_bond','trade_escrow') + list_display_links = ['id'] + pass + +@admin.register(LNPayment) +class LNPaymentAdmin(admin.ModelAdmin): + list_display = ('id','concept','status','amount','type','invoice','secret','expires_at','sender','receiver') list_display_links = ['id'] pass diff --git a/api/lightning.py b/api/lightning.py new file mode 100644 index 00000000..50d60529 --- /dev/null +++ b/api/lightning.py @@ -0,0 +1,41 @@ +import random +import string + +####### +# Placeholder functions +# Should work with LND (maybe c-lightning in the future) + +class LNNode(): + ''' + Place holder functions to interact with Lightning Node + ''' + + def gen_hodl_invoice(): + '''Generates hodl invoice to publish an order''' + return ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) + + def validate_hodl_invoice_locked(): + '''Generates hodl invoice to publish an order''' + return True + + def validate_ln_invoice(invoice): + '''Checks if a LN invoice is valid''' + return True + + def pay_buyer_invoice(invoice): + '''Sends sats to buyer''' + return True + + def charge_hodl_htlcs(invoice): + '''Charges a LN hodl invoice''' + return True + + def free_hodl_htlcs(invoice): + '''Returns sats''' + return True + + + + + + diff --git a/api/models.py b/api/models.py index 15eaf3ef..778113c8 100644 --- a/api/models.py +++ b/api/models.py @@ -11,8 +11,50 @@ from pathlib import Path ############################# # TODO # Load hparams from .env file -min_satoshis_trade = 10*1000 -max_satoshis_trade = 500*1000 + +MIN_TRADE = 10*1000 #In sats +MAX_TRADE = 500*1000 +FEE = 0.002 # Trade fee in % +BOND_SIZE = 0.01 # Bond in % + + +class LNPayment(models.Model): + + class Types(models.IntegerChoices): + NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hodl) + HODL = 1, 'Hodl invoice' + + class Concepts(models.IntegerChoices): + MAKEBOND = 0, 'Maker bond' + TAKEBOND = 1, 'Taker-buyer bond' + TRESCROW = 2, 'Trade escrow' + PAYBUYER = 3, 'Payment to buyer' + + class Status(models.IntegerChoices): + INVGEN = 0, 'Hodl invoice was generated' + LOCKED = 1, 'Hodl invoice has HTLCs locked' + CHRGED = 2, 'Hodl invoice was charged' + RETNED = 3, 'Hodl invoice was returned' + MISSNG = 4, 'Buyer invoice is missing' + IVALID = 5, 'Buyer invoice is valid' + INPAID = 6, 'Buyer invoice was paid' + INFAIL = 7, 'Buyer invoice routing failed' + + # payment use case + type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL) + concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND) + status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN) + + # payment details + invoice = models.CharField(max_length=300, unique=False, null=True, default=None) + secret = models.CharField(max_length=300, unique=False, null=True, default=None) + expires_at = models.DateTimeField() + amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) + + # payment relationals + sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None) + receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None) + class Order(models.Model): @@ -25,7 +67,7 @@ class Order(models.Model): EUR = 2, 'EUR' ETH = 3, 'ETH' - class Status(models.TextChoices): + class Status(models.IntegerChoices): WFB = 0, 'Waiting for bond' PUB = 1, 'Published in order book' DEL = 2, 'Deleted from order book' @@ -48,36 +90,37 @@ class Order(models.Model): EXP = 19, 'Expired' # order info - status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=int(Status.WFB)) + status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() # order details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) currency = models.PositiveSmallIntegerField(choices=Currencies.choices, null=False) - amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)]) + amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) payment_method = models.CharField(max_length=30, null=False, default="Not specified") - premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)]) - satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(min_satoshis_trade), MaxValueValidator(max_satoshis_trade)]) - is_explicit = models.BooleanField(default=False, null=False) # 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) + # marked to marked + premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)]) + t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) + # explicit + satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)]) + # 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 taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a taker can only take one order # order collateral - has_maker_bond = models.BooleanField(default=False, null=False) - has_taker_bond = models.BooleanField(default=False, null=False) - has_trade_collat = models.BooleanField(default=False, null=False) - - maker_bond_secret = models.CharField(max_length=300, unique=False, null=True, default=None) - taker_bond_secret = models.CharField(max_length=300, unique=False, null=True, default=None) - trade_collat_secret = models.CharField(max_length=300, unique=False, null=True, default=None) + maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None) + taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None) + trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None) # buyer payment LN invoice - has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid - invoice = models.CharField(max_length=300, unique=False, null=True, default=None) - + buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None) + + class Profile(models.Model): user = models.OneToOneField(User,on_delete=models.CASCADE) diff --git a/api/views.py b/api/views.py index a5fc6ef4..2adc38a7 100644 --- a/api/views.py +++ b/api/views.py @@ -8,7 +8,8 @@ from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer -from .models import Order +from .models import Order, LNPayment +from .lightning import LNNode from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -39,11 +40,6 @@ def validate_already_maker_or_taker(request): return True, None -def validate_ln_invoice(invoice): - '''Checks if a LN invoice is valid''' - #TODO - return True - # Create your views here. class OrderMakerView(CreateAPIView): @@ -68,7 +64,7 @@ class OrderMakerView(CreateAPIView): # Creates a new order in db order = Order( type=otype, - status=int(Order.Status.PUB), # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) + status=Order.Status.PUB, # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) currency=currency, amount=amount, payment_method=payment_method, @@ -105,11 +101,11 @@ class OrderView(viewsets.ViewSet): 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 == int(Order.Types.BUY)) or (data['is_taker'] and order.type == int(Order.Types.SELL)) - data['is_seller'] = (data['is_maker'] and order.type == int(Order.Types.SELL)) or (data['is_taker'] and order.type == int(Order.Types.BUY)) + 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 != int(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 nicks too @@ -142,28 +138,28 @@ class OrderView(viewsets.ViewSet): invoice = serializer.data.get('invoice') # If this is an empty POST request (no invoice), it must be taker request! - if not invoice and order.status == int(Order.Status.PUB): + if not invoice and order.status == Order.Status.PUB: valid, response = validate_already_maker_or_taker(request) if not valid: return response order.taker = self.request.user - order.status = int(Order.Status.TAK) + order.status = Order.Status.TAK #TODO REPLY WITH HODL INVOICE data = ListOrderSerializer(order).data # An invoice came in! update it elif invoice: - if validate_ln_invoice(invoice): + if LNNode.validate_ln_invoice(invoice): order.invoice = invoice #TODO Validate if request comes from PARTICIPANT AND BUYER #If the order status was Payment Failed. Move foward to invoice Updated. - if order.status == int(Order.Status.FAI): - order.status = int(Order.Status.UPI) + if order.status == Order.Status.FAI: + order.status = Order.Status.UPI else: return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}) diff --git a/frontend/src/components/TradePipelineBox.js b/frontend/src/components/TradePipelineBox.js new file mode 100644 index 00000000..e69de29b