mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 19:06:26 +00:00
Add logics module
This commit is contained in:
parent
5640b11e6f
commit
46c129bf80
@ -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')
|
||||
|
||||
|
@ -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'''
|
||||
|
@ -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
|
||||
|
@ -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']
|
56
api/views.py
56
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):
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user