Add logics module

This commit is contained in:
Reckless_Satoshi 2022-01-06 04:32:17 -08:00
parent 5640b11e6f
commit 46c129bf80
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
7 changed files with 113 additions and 68 deletions

View File

@ -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')

View File

@ -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'''

View File

@ -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('<img src="%s" width="50" height="50" />' % 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

View File

@ -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')
fields = ('id','buyer_invoice')
class UpdateInvoiceSerializer(serializers.ModelSerializer):
class Meta:
model = LNPayment
fields = ['invoice']

View File

@ -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):

View File

@ -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 {
<Typography variant="subtitle1" color="text.secondary">
Payment via <b>{order.payment_method}</b>
</Typography>
{/*
<Typography variant="subtitle1" color="text.secondary">
Priced {order.is_explicit ?
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
" at " +
parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market"
)}
</Typography>
</Typography> */}
<Typography variant="subtitle1" color="text.secondary">
<b>{" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC</b> (Binance API)
@ -176,13 +176,13 @@ export default class BookPage extends Component {
<Typography component="h5" variant="h5">
No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode}
</Typography>
</Grid>
<Grid item>
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
</Grid>
<Typography component="body1" variant="body1">
Be the first one to create an order
</Typography>
</Grid>
<Grid item>
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
</Grid>
</Grid>)
: this.bookCards()
}

View File

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