mirror of
https://github.com/RoboSats/robosats.git
synced 2025-07-19 09:13:28 +00:00
Add logics module
This commit is contained in:
@ -30,7 +30,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(LNPayment)
|
@admin.register(LNPayment)
|
||||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
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')
|
list_display_links = ('id','concept')
|
||||||
change_links = ('sender','receiver')
|
change_links = ('sender','receiver')
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
@ -18,9 +20,15 @@ class LNNode():
|
|||||||
'''Generates hodl invoice to publish an order'''
|
'''Generates hodl invoice to publish an order'''
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def validate_ln_invoice(invoice):
|
def validate_ln_invoice(invoice): # num_satoshis
|
||||||
'''Checks if a LN invoice is valid'''
|
'''Checks if the submited LN invoice is as expected'''
|
||||||
return True
|
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):
|
def pay_buyer_invoice(invoice):
|
||||||
'''Sends sats to buyer'''
|
'''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.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .lightning import LNNode
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# TODO
|
# TODO
|
||||||
# Load hparams from .env file
|
# Load hparams from .env file
|
||||||
@ -16,7 +17,7 @@ MIN_TRADE = 10*1000 #In sats
|
|||||||
MAX_TRADE = 500*1000
|
MAX_TRADE = 500*1000
|
||||||
FEE = 0.002 # Trade fee in %
|
FEE = 0.002 # Trade fee in %
|
||||||
BOND_SIZE = 0.01 # Bond in %
|
BOND_SIZE = 0.01 # Bond in %
|
||||||
|
ESCROW_USERNAME = 'admin'
|
||||||
|
|
||||||
class LNPayment(models.Model):
|
class LNPayment(models.Model):
|
||||||
|
|
||||||
@ -33,25 +34,28 @@ class LNPayment(models.Model):
|
|||||||
class Status(models.IntegerChoices):
|
class Status(models.IntegerChoices):
|
||||||
INVGEN = 0, 'Hodl invoice was generated'
|
INVGEN = 0, 'Hodl invoice was generated'
|
||||||
LOCKED = 1, 'Hodl invoice has HTLCs locked'
|
LOCKED = 1, 'Hodl invoice has HTLCs locked'
|
||||||
CHRGED = 2, 'Hodl invoice was charged'
|
SETLED = 2, 'Invoice settled'
|
||||||
RETNED = 3, 'Hodl invoice was returned'
|
RETNED = 3, 'Hodl invoice was returned'
|
||||||
MISSNG = 4, 'Buyer invoice is missing'
|
MISSNG = 4, 'Buyer invoice is missing'
|
||||||
IVALID = 5, 'Buyer invoice is valid'
|
VALIDI = 5, 'Buyer invoice is valid'
|
||||||
INPAID = 6, 'Buyer invoice was paid'
|
INFAIL = 6, 'Buyer invoice routing failed'
|
||||||
INFAIL = 7, 'Buyer invoice routing failed'
|
|
||||||
|
|
||||||
# payment use case
|
# payment use details
|
||||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL)
|
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL)
|
||||||
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
||||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
|
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)
|
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()
|
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)
|
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)
|
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 payment LN invoice
|
||||||
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
class Profile(models.Model):
|
||||||
|
|
||||||
user = models.OneToOneField(User,on_delete=models.CASCADE)
|
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
|
# method to create a fake table field in read only mode
|
||||||
def avatar_tag(self):
|
def avatar_tag(self):
|
||||||
return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar())
|
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 rest_framework import serializers
|
||||||
from .models import Order
|
from .models import Order, LNPayment
|
||||||
|
|
||||||
class ListOrderSerializer(serializers.ModelSerializer):
|
class ListOrderSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -15,3 +15,8 @@ class UpdateOrderSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ('id','buyer_invoice')
|
fields = ('id','buyer_invoice')
|
||||||
|
|
||||||
|
class UpdateInvoiceSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = LNPayment
|
||||||
|
fields = ['invoice']
|
54
api/views.py
54
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 import authenticate, login, logout
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
|
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateInvoiceSerializer
|
||||||
from .models import Order, LNPayment
|
from .models import Order, LNPayment, Logics
|
||||||
from .lightning import LNNode
|
from .lightning import LNNode
|
||||||
|
|
||||||
from .nick_generator.nick_generator import NickGenerator
|
from .nick_generator.nick_generator import NickGenerator
|
||||||
@ -27,19 +27,6 @@ expiration_time = 8
|
|||||||
avatar_path = Path('frontend/static/assets/avatars')
|
avatar_path = Path('frontend/static/assets/avatars')
|
||||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
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.
|
# Create your views here.
|
||||||
|
|
||||||
class OrderMakerView(CreateAPIView):
|
class OrderMakerView(CreateAPIView):
|
||||||
@ -57,9 +44,9 @@ class OrderMakerView(CreateAPIView):
|
|||||||
satoshis = serializer.data.get('satoshis')
|
satoshis = serializer.data.get('satoshis')
|
||||||
is_explicit = serializer.data.get('is_explicit')
|
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:
|
if not valid:
|
||||||
return response
|
return Response(context, status=status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
# Creates a new order in db
|
# Creates a new order in db
|
||||||
order = Order(
|
order = Order(
|
||||||
@ -82,7 +69,7 @@ class OrderMakerView(CreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrderView(viewsets.ViewSet):
|
class OrderView(viewsets.ViewSet):
|
||||||
serializer_class = UpdateOrderSerializer
|
serializer_class = UpdateInvoiceSerializer
|
||||||
lookup_url_kwarg = 'order_id'
|
lookup_url_kwarg = 'order_id'
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
@ -129,44 +116,31 @@ class OrderView(viewsets.ViewSet):
|
|||||||
def take_or_update(self, request, format=None):
|
def take_or_update(self, request, format=None):
|
||||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
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)
|
order = Order.objects.get(id=order_id)
|
||||||
|
|
||||||
if serializer.is_valid():
|
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 this is an empty POST request (no invoice), it must be taker request!
|
||||||
if not invoice and order.status == Order.Status.PUB:
|
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)
|
||||||
|
|
||||||
valid, response = validate_already_maker_or_taker(request)
|
Logics.take(order, request.user)
|
||||||
if not valid:
|
|
||||||
return response
|
|
||||||
|
|
||||||
order.taker = self.request.user
|
|
||||||
order.status = Order.Status.TAK
|
|
||||||
|
|
||||||
#TODO REPLY WITH HODL INVOICE
|
|
||||||
data = ListOrderSerializer(order).data
|
|
||||||
|
|
||||||
# An invoice came in! update it
|
# An invoice came in! update it
|
||||||
elif invoice:
|
elif invoice:
|
||||||
if LNNode.validate_ln_invoice(invoice):
|
print(invoice)
|
||||||
order.invoice = invoice
|
updated = Logics.update_invoice(order=order,user=request.user,invoice=invoice)
|
||||||
|
if not updated:
|
||||||
#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:
|
|
||||||
return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'})
|
return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'})
|
||||||
|
|
||||||
# Something else is going on. Probably not allowed.
|
# Something else is going on. Probably not allowed.
|
||||||
else:
|
else:
|
||||||
return Response({'bad_request':'Not allowed'})
|
return Response({'bad_request':'Not allowed'})
|
||||||
|
|
||||||
order.save()
|
|
||||||
return self.get(request)
|
return self.get(request)
|
||||||
|
|
||||||
class UserView(APIView):
|
class UserView(APIView):
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from "react";
|
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 {
|
export default class BookPage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -13,7 +14,6 @@ export default class BookPage extends Component {
|
|||||||
this.state.currencyCode = this.getCurrencyCode(this.state.currency)
|
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
|
// Show message to be the first one to make an order
|
||||||
getOrderDetails() {
|
getOrderDetails() {
|
||||||
fetch('/api/book' + '?currency=' + this.state.currency + "&type=" + this.state.type)
|
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">
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
◑ Payment via <b>{order.payment_method}</b>
|
◑ Payment via <b>{order.payment_method}</b>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{/*
|
||||||
<Typography variant="subtitle1" color="text.secondary">
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
◑ Priced {order.is_explicit ?
|
◑ Priced {order.is_explicit ?
|
||||||
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
|
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
|
||||||
" at " +
|
" at " +
|
||||||
parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market"
|
parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market"
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography> */}
|
||||||
|
|
||||||
<Typography variant="subtitle1" color="text.secondary">
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
◑ <b>{" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC</b> (Binance API)
|
◑ <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">
|
<Typography component="h5" variant="h5">
|
||||||
No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode}
|
No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||||
|
</Grid>
|
||||||
<Typography component="body1" variant="body1">
|
<Typography component="body1" variant="body1">
|
||||||
Be the first one to create an order
|
Be the first one to create an order
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>)
|
</Grid>)
|
||||||
: this.bookCards()
|
: this.bookCards()
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ export default class UserGenPage extends Component {
|
|||||||
delGeneratedUser() {
|
delGeneratedUser() {
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken},
|
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
||||||
};
|
};
|
||||||
fetch("/api/usergen", requestOptions)
|
fetch("/api/usergen", requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
|
Reference in New Issue
Block a user