diff --git a/api/admin.py b/api/admin.py index 1d0bd415..f496caa8 100644 --- a/api/admin.py +++ b/api/admin.py @@ -30,7 +30,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): @admin.register(LNPayment) class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('id','concept','status','amount','type','invoice','secret','expires_at','sender_link','receiver_link') + list_display = ('id','concept','status','num_satoshis','type','invoice','preimage','expires_at','sender_link','receiver_link') list_display_links = ('id','concept') change_links = ('sender','receiver') diff --git a/api/lightning.py b/api/lightning.py index 50d60529..de480279 100644 --- a/api/lightning.py +++ b/api/lightning.py @@ -1,3 +1,5 @@ +from django.utils import timezone + import random import string @@ -18,9 +20,15 @@ class LNNode(): '''Generates hodl invoice to publish an order''' return True - def validate_ln_invoice(invoice): - '''Checks if a LN invoice is valid''' - return True + def validate_ln_invoice(invoice): # num_satoshis + '''Checks if the submited LN invoice is as expected''' + valid = True + num_satoshis = 50000 # TODO decrypt and confirm sats are as expected + description = 'Placeholder desc' # TODO decrypt from LN invoice + payment_hash = '567126' # TODO decrypt + expires_at = timezone.now() # TODO decrypt + + return valid, num_satoshis, description, payment_hash, expires_at def pay_buyer_invoice(invoice): '''Sends sats to buyer''' diff --git a/api/models.py b/api/models.py index 452e57e3..b5cb5135 100644 --- a/api/models.py +++ b/api/models.py @@ -3,11 +3,12 @@ from django.contrib.auth.models import User from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver - from django.utils.html import mark_safe from pathlib import Path +from .lightning import LNNode + ############################# # TODO # Load hparams from .env file @@ -16,7 +17,7 @@ MIN_TRADE = 10*1000 #In sats MAX_TRADE = 500*1000 FEE = 0.002 # Trade fee in % BOND_SIZE = 0.01 # Bond in % - +ESCROW_USERNAME = 'admin' class LNPayment(models.Model): @@ -33,25 +34,28 @@ class LNPayment(models.Model): class Status(models.IntegerChoices): INVGEN = 0, 'Hodl invoice was generated' LOCKED = 1, 'Hodl invoice has HTLCs locked' - CHRGED = 2, 'Hodl invoice was charged' + SETLED = 2, 'Invoice settled' 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' + VALIDI = 5, 'Buyer invoice is valid' + INFAIL = 6, 'Buyer invoice routing failed' - # payment use case + # payment use details 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) + routing_retries = models.PositiveSmallIntegerField(null=False, default=0) - # payment details + # payment info invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) - secret = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + payment_hash = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + preimage = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + description = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() - amount = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) + num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) - # payment relationals + # involved parties 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) @@ -123,7 +127,6 @@ class Order(models.Model): # buyer payment LN invoice buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True) - class Profile(models.Model): user = models.OneToOneField(User,on_delete=models.CASCADE) @@ -166,3 +169,58 @@ class Profile(models.Model): # method to create a fake table field in read only mode def avatar_tag(self): return mark_safe('' % self.get_avatar()) + +class Logics(): + + def validate_already_maker_or_taker(user): + '''Checks if the user is already partipant of an order''' + queryset = Order.objects.filter(maker=user) + if queryset.exists(): + return False, {'Bad Request':'You are already maker of an order'} + queryset = Order.objects.filter(taker=user) + if queryset.exists(): + return False, {'Bad Request':'You are already taker of an order'} + return True, None + + def take(order, user): + order.taker = user + order.status = Order.Status.TAK + order.save() + + def is_buyer(order, user): + is_maker = order.maker == user + is_taker = order.taker == user + return (is_maker and order.type == Order.Types.BUY) or (is_taker and order.type == Order.Types.SELL) + + def is_seller(order, user): + is_maker = order.maker == user + is_taker = order.taker == user + return (is_maker and order.type == Order.Types.SELL) or (is_taker and order.type == Order.Types.BUY) + + @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, + concept = LNPayment.Concepts.PAYBUYER, + type = LNPayment.Types.NORM, + sender = User.objects.get(username=ESCROW_USERNAME), + # if there is a LNPayment matching these above, it updates that with defaults below. + defaults={ + 'invoice' : invoice, + 'status' : LNPayment.Status.VALIDI, + 'num_satoshis' : num_satoshis, + 'description' : description, + 'payment_hash' : payment_hash, + 'expires_at' : expires_at} + ) + + #If the order status was Payment Failed. Move foward to invoice Updated. + if order.status == Order.Status.FAI: + order.status = Order.Status.UPI + order.save() + return True + + return False diff --git a/api/serializers.py b/api/serializers.py index c88b14b8..7298b77d 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Order +from .models import Order, LNPayment class ListOrderSerializer(serializers.ModelSerializer): class Meta: @@ -14,4 +14,9 @@ class MakeOrderSerializer(serializers.ModelSerializer): class UpdateOrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ('id','buyer_invoice') \ No newline at end of file + fields = ('id','buyer_invoice') + +class UpdateInvoiceSerializer(serializers.ModelSerializer): + class Meta: + model = LNPayment + fields = ['invoice'] \ No newline at end of file diff --git a/api/views.py b/api/views.py index 04fc6d42..1a7eaadd 100644 --- a/api/views.py +++ b/api/views.py @@ -7,8 +7,8 @@ from rest_framework.response import Response 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, LNPayment +from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateInvoiceSerializer +from .models import Order, LNPayment, Logics from .lightning import LNNode from .nick_generator.nick_generator import NickGenerator @@ -27,19 +27,6 @@ expiration_time = 8 avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) -def validate_already_maker_or_taker(request): - '''Checks if the user is already partipant of an order''' - - queryset = Order.objects.filter(maker=request.user.id) - if queryset.exists(): - return False, Response({'Bad Request':'You are already maker of an order'}, status=status.HTTP_400_BAD_REQUEST) - - queryset = Order.objects.filter(taker=request.user.id) - if queryset.exists(): - return False, Response({'Bad Request':'You are already taker of an order'}, status=status.HTTP_400_BAD_REQUEST) - - return True, None - # Create your views here. class OrderMakerView(CreateAPIView): @@ -57,9 +44,9 @@ class OrderMakerView(CreateAPIView): satoshis = serializer.data.get('satoshis') is_explicit = serializer.data.get('is_explicit') - valid, response = validate_already_maker_or_taker(request) + valid, context = Logics.validate_already_maker_or_taker(request.user) if not valid: - return response + return Response(context, status=status.HTTP_409_CONFLICT) # Creates a new order in db order = Order( @@ -82,7 +69,7 @@ class OrderMakerView(CreateAPIView): class OrderView(viewsets.ViewSet): - serializer_class = UpdateOrderSerializer + serializer_class = UpdateInvoiceSerializer lookup_url_kwarg = 'order_id' def get(self, request, format=None): @@ -129,44 +116,31 @@ class OrderView(viewsets.ViewSet): def take_or_update(self, request, format=None): order_id = request.GET.get(self.lookup_url_kwarg) - serializer = UpdateOrderSerializer(data=request.data) + serializer = UpdateInvoiceSerializer(data=request.data) order = Order.objects.get(id=order_id) if serializer.is_valid(): - invoice = serializer.data.get('buyer_invoice') + 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 == Order.Status.PUB: - - valid, response = validate_already_maker_or_taker(request) - if not valid: - return response + if not invoice and order.status == Order.Status.PUB: + valid, context = Logics.validate_already_maker_or_taker(request.user) + if not valid: return Response(context, status=status.HTTP_409_CONFLICT) - order.taker = self.request.user - order.status = Order.Status.TAK - - #TODO REPLY WITH HODL INVOICE - data = ListOrderSerializer(order).data + Logics.take(order, request.user) # An invoice came in! update it elif 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 == Order.Status.FAI: - order.status = Order.Status.UPI - - else: + print(invoice) + updated = Logics.update_invoice(order=order,user=request.user,invoice=invoice) + if not updated: return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}) # Something else is going on. Probably not allowed. else: return Response({'bad_request':'Not allowed'}) - order.save() return self.get(request) class UserView(APIView): diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 93cb63d8..bdf30d83 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -1,5 +1,6 @@ import React, { Component } from "react"; -import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, Link, RouterLink, ListItemAvatar} from "@material-ui/core" +import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@material-ui/core" +import { Link } from 'react-router-dom' export default class BookPage extends Component { constructor(props) { @@ -13,7 +14,6 @@ export default class BookPage extends Component { this.state.currencyCode = this.getCurrencyCode(this.state.currency) } - // Fix needed to handle HTTP 404 error when no order is found // Show message to be the first one to make an order getOrderDetails() { fetch('/api/book' + '?currency=' + this.state.currency + "&type=" + this.state.type) @@ -90,14 +90,14 @@ export default class BookPage extends Component { ◑ Payment via {order.payment_method} - +{/* ◑ Priced {order.is_explicit ? " explicitly at " + this.pn(order.satoshis) + " Sats" : ( " at " + parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market" )} - + */} {" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC (Binance API) @@ -176,13 +176,13 @@ export default class BookPage extends Component { No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode} + + + + Be the first one to create an order - - - - ) : this.bookCards() } diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index 0332043b..ef7d9c98 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -58,7 +58,7 @@ export default class UserGenPage extends Component { delGeneratedUser() { const requestOptions = { method: 'DELETE', - headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, }; fetch("/api/usergen", requestOptions) .then((response) => response.json())