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())