Add more api logics

The workflow is actually more complex than I though. In fact the whole scope of the project greatly surpass my expectation of "weekend project".  Want to lay down something functional even if buggy and ugly, I'm a bad coder but this will work out!
This commit is contained in:
Reckless_Satoshi 2022-01-07 03:31:33 -08:00
parent 6a1a906bea
commit 8a55383761
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
5 changed files with 213 additions and 110 deletions

View File

@ -29,18 +29,18 @@ class Logics():
'''Checks if the user is already partipant of an order''' '''Checks if the user is already partipant of an order'''
queryset = Order.objects.filter(maker=user) queryset = Order.objects.filter(maker=user)
if queryset.exists(): if queryset.exists():
return False, {'Bad Request':'You are already maker of an order'} return False, {'bad_request':'You are already maker of an order'}
queryset = Order.objects.filter(taker=user) queryset = Order.objects.filter(taker=user)
if queryset.exists(): if queryset.exists():
return False, {'Bad Request':'You are already taker of an order'} return False, {'bad_request':'You are already taker of an order'}
return True, None return True, None
def validate_order_size(order): def validate_order_size(order):
'''Checks if order is withing limits at t0''' '''Checks if order is withing limits at t0'''
if order.t0_satoshis > MAX_TRADE: if order.t0_satoshis > MAX_TRADE:
return False, {'Bad_request': 'Your order is too big. It is worth {order.t0_satoshis} now, max is {MAX_TRADE}'} return False, {'bad_request': f'Your order is too big. It is worth {order.t0_satoshis} now. But maximum is {MAX_TRADE}'}
if order.t0_satoshis < MIN_TRADE: if order.t0_satoshis < MIN_TRADE:
return False, {'Bad_request': 'Your order is too small. It is worth {order.t0_satoshis} now, min is {MIN_TRADE}'} return False, {'bad_request': f'Your order is too small. It is worth {order.t0_satoshis} now. But minimum is {MIN_TRADE}'}
return True, None return True, None
def take(order, user): def take(order, user):
@ -64,11 +64,12 @@ class Logics():
satoshis_now = order.satoshis satoshis_now = order.satoshis
else: else:
# TODO Add fallback Public APIs and error handling # TODO Add fallback Public APIs and error handling
# Think about polling price data in a different way (e.g. store locally every t seconds)
market_prices = requests.get(MARKET_PRICE_API).json() market_prices = requests.get(MARKET_PRICE_API).json()
exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last'])
satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000
return satoshis_now return int(satoshis_now)
def order_expires(order): def order_expires(order):
order.status = Order.Status.EXP order.status = Order.Status.EXP
@ -76,6 +77,16 @@ class Logics():
order.taker = None order.taker = None
order.save() order.save()
@classmethod
def buyer_invoice_amount(cls, order, user):
''' Computes buyer invoice amount. Uses order.last_satoshis,
that is the final trade amount set at Taker Bond time'''
if cls.is_buyer(order, user):
invoice_amount = int(order.last_satoshis * (1-FEE)) # Trading FEE is charged here.
return True, {'invoice_amount': invoice_amount}
@classmethod @classmethod
def update_invoice(cls, order, user, invoice): def update_invoice(cls, order, user, invoice):
is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice)
@ -117,7 +128,21 @@ class Logics():
order.save() order.save()
return True, None return True, None
@classmethod
def rate_counterparty(cls, order, user, rating):
# if maker, rates taker
if order.maker == user:
order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1
last_ratings = list(order.taker.profile.last_ratings).append(rating)
order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
# if taker, rates maker
if order.taker == user:
order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1
last_ratings = list(order.maker.profile.last_ratings).append(rating)
order.maker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
order.save()
return True, None
@classmethod @classmethod
def cancel_order(cls, order, user, state): def cancel_order(cls, order, user, state):
@ -140,12 +165,12 @@ class Logics():
''' The order goes back to the book as public. ''' The order goes back to the book as public.
LNPayment "order.taker_bond" is deleted() ''' LNPayment "order.taker_bond" is deleted() '''
# 4) When taker or maker cancel after bond # 4) When taker or maker cancel after bond (before escrow)
'''The order goes into cancelled status if maker cancels. '''The order goes into cancelled status if maker cancels.
The order goes into the public book if taker cancels. The order goes into the public book if taker cancels.
In both cases there is a small fee.''' In both cases there is a small fee.'''
# 5) When trade collateral has been posted # 5) When trade collateral has been posted (after escrow)
'''Always goes to cancelled status. Collaboration is needed. '''Always goes to cancelled status. Collaboration is needed.
When a user asks for cancel, 'order.is_pending_cancel' goes True. When a user asks for cancel, 'order.is_pending_cancel' goes True.
When the second user asks for cancel. Order is totally cancelled. When the second user asks for cancel. Order is totally cancelled.
@ -154,22 +179,23 @@ class Logics():
else: else:
return False, {'bad_request':'You cannot cancel this order'} return False, {'bad_request':'You cannot cancel this order'}
@classmethod @classmethod
def gen_maker_hodl_invoice(cls, order, user): def gen_maker_hodl_invoice(cls, order, user):
# Do not gen and delete if order is more than 5 minutes old # Do not gen and cancel if order is more than 5 minutes old
if order.expires_at < timezone.now(): if order.expires_at < timezone.now():
cls.order_expires(order) cls.order_expires(order)
return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} return False, {'bad_request':'Invoice expired. You did not confirm publishing the order in time. Make a new order.'}
# Return the previous invoice if there was one # Return the previous invoice if there was one and is still unpaid
if order.maker_bond: if order.maker_bond:
return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} if order.maker_bond.status == LNPayment.Status.INVGEN:
return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis}
else:
return False, None
order.satoshis_now = cls.satoshis_now(order) order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.satoshis_now * BOND_SIZE) bond_satoshis = int(order.last_satoshis * BOND_SIZE)
description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.' description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.'
# Gen HODL Invoice # Gen HODL Invoice
@ -188,22 +214,27 @@ class Logics():
expires_at = expires_at) expires_at = expires_at)
order.save() order.save()
return True, {'invoice':invoice,'bond_satoshis':bond_satoshis} return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis}
@classmethod @classmethod
def gen_taker_hodl_invoice(cls, order, user): def gen_taker_hodl_invoice(cls, order, user):
# Do not gen and cancel if a taker invoice is there and older than 2 minutes # Do not gen and cancel if a taker invoice is there and older than X minutes and unpaid still
if order.taker_bond: if order.taker_bond:
# Check if status is INVGEN and still not expired
if order.taker_bond.status == LNPayment.Status.INVGEN:
if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)): if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)):
cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond
return False, {'Invoice expired':'You did not confirm taking the order in time.'} return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
# Return the previous invoice there was with INVGEN status
else: else:
# Return the previous invoice if there was one return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
return True, {'invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis} # Invoice exists, but was already locked or settled
else:
return False, None
order.satoshis_now = cls.satoshis_now(order) order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE
bond_satoshis = int(order.satoshis_now * BOND_SIZE) bond_satoshis = int(order.last_satoshis * BOND_SIZE)
description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat.' description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat.'
# Gen HODL Invoice # Gen HODL Invoice
@ -222,4 +253,42 @@ class Logics():
expires_at = expires_at) expires_at = expires_at)
order.save() order.save()
return True, {'invoice':invoice,'bond_satoshis': bond_satoshis} return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis}
@classmethod
def gen_escrow_hodl_invoice(cls, order, user):
# Do not generate and cancel if an invoice is there and older than X minutes and unpaid still
if order.trade_escrow:
# Check if status is INVGEN and still not expired
if order.taker_bond.status == LNPayment.Status.INVGEN:
if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TRADE_ESCR_INVOICE)): # Expired
cls.cancel_order(order, user, 4) # State 4, cancel order before trade escrow locked
return False, {'bad_request':'Invoice expired. You did not lock the trade escrow in time.'}
# Return the previous invoice there was with INVGEN status
else:
return True, {'escrow_invoice':order.trade_escrow.invoice,'escrow_satoshis':order.trade_escrow.num_satoshis}
# Invoice exists, but was already locked or settled
else:
return False, None # Does not return any context of a healthy locked escrow
escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis)
description = f'RoboSats - Escrow amount for {str(order)} - This escrow will be released to the buyer once you confirm you received the fiat.'
# Gen HODL Invoice
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
order.taker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.TRESCROW,
type = LNPayment.Types.HODL,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
status = LNPayment.Status.INVGEN,
num_satoshis = escrow_satoshis,
description = description,
payment_hash = payment_hash,
expires_at = expires_at)
order.save()
return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis}

View File

@ -27,18 +27,18 @@ class LNPayment(models.Model):
class Concepts(models.IntegerChoices): class Concepts(models.IntegerChoices):
MAKEBOND = 0, 'Maker bond' MAKEBOND = 0, 'Maker bond'
TAKEBOND = 1, 'Taker-buyer bond' TAKEBOND = 1, 'Taker bond'
TRESCROW = 2, 'Trade escrow' TRESCROW = 2, 'Trade escrow'
PAYBUYER = 3, 'Payment to buyer' PAYBUYER = 3, 'Payment to buyer'
class Status(models.IntegerChoices): class Status(models.IntegerChoices):
INVGEN = 0, 'Hodl invoice was generated' INVGEN = 0, 'Generated'
LOCKED = 1, 'Hodl invoice has HTLCs locked' LOCKED = 1, 'Locked'
SETLED = 2, 'Invoice settled' SETLED = 2, 'Settled'
RETNED = 3, 'Hodl invoice was returned' RETNED = 3, 'Returned'
MISSNG = 4, 'Buyer invoice is missing' MISSNG = 4, 'Missing'
VALIDI = 5, 'Buyer invoice is valid' VALIDI = 5, 'Valid'
INFAIL = 6, 'Buyer invoice routing failed' INFAIL = 6, 'Failed routing'
# payment use details # 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)
@ -59,8 +59,7 @@ class LNPayment(models.Model):
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)
def __str__(self): def __str__(self):
# Make relational back to ORDER return (f'HTLC {self.id}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
return (f'HTLC {self.id}: {self.Concepts(self.concept).label}')
class Order(models.Model): class Order(models.Model):
@ -130,7 +129,7 @@ class Order(models.Model):
def __str__(self): def __str__(self):
# Make relational back to ORDER # Make relational back to ORDER
return (f'Order {self.id}: {self.Types(self.type).label} {"{:,}".format(self.t0_satoshis)} Sats for {self.Currencies(self.currency).label}') return (f'Order {self.id}: {self.Types(self.type).label} BTC for {self.amount} {self.Currencies(self.currency).label}')
@receiver(pre_delete, sender=Order) @receiver(pre_delete, sender=Order)
def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs):

View File

@ -7,7 +7,7 @@ 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, UpdateOrderSerializer
from .models import Order from .models import LNPayment, Order
from .logics import Logics from .logics import Logics
from .nick_generator.nick_generator import NickGenerator from .nick_generator.nick_generator import NickGenerator
@ -74,15 +74,21 @@ class OrderView(viewsets.ViewSet):
lookup_url_kwarg = 'order_id' lookup_url_kwarg = 'order_id'
def get(self, request, format=None): def get(self, request, format=None):
'''
Full trade pipeline takes place while looking/refreshing the order page.
'''
order_id = request.GET.get(self.lookup_url_kwarg) order_id = request.GET.get(self.lookup_url_kwarg)
if order_id == None: if order_id == None:
return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) return Response({'bad_request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
order = Order.objects.filter(id=order_id) order = Order.objects.filter(id=order_id)
# check if exactly one order is found in the db # check if exactly one order is found in the db
if len(order) == 1 : if len(order) != 1 :
return Response({'bad_request':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND)
# This is our order.
order = order[0] order = order[0]
# 1) If order expired # 1) If order expired
@ -104,7 +110,7 @@ class OrderView(viewsets.ViewSet):
# 3) If not a participant and order is not public, forbid. # 3) If not a participant and order is not public, forbid.
if not data['is_participant'] and order.status != Order.Status.PUB: if not data['is_participant'] and order.status != Order.Status.PUB:
return Response({'bad_request':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN)
# 4) Non participants can view details (but only if PUB) # 4) Non participants can view details (but only if PUB)
elif not data['is_participant'] and order.status != Order.Status.PUB: elif not data['is_participant'] and order.status != Order.Status.PUB:
@ -133,16 +139,35 @@ class OrderView(viewsets.ViewSet):
else: else:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
# 7) If status is 'WF2'or'WTC' and user is Seller, reply with an ESCROW HODL invoice. # 7) If status is 'WF2'or'WTC'
elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE) and data['is_seller']: elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE):
valid, context = Logics.gen_seller_hodl_invoice(order, request.user)
# If the two bonds are locked
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
# 7.a) And if user is Seller, reply with an ESCROW HODL invoice.
if data['is_seller']:
valid, context = Logics.gen_escrow_hodl_invoice(order, request.user)
if valid: if valid:
data = {**data, **context} data = {**data, **context}
else: else:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
# 7.b) If user is Buyer, reply with an AMOUNT so he can send the buyer invoice.
elif data['is_buyer']:
valid, context = Logics.buyer_invoice_amount(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 8) If status is 'CHA'or '' or '' and all HTLCS are in LOCKED
elif order.status == Order.Status.CHA: # TODO Add the other status
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
# add whether a collaborative cancel is pending
data['pending_cancel'] = order.is_pending_cancel
return Response(data, status.HTTP_200_OK) return Response(data, status.HTTP_200_OK)
return Response({'Order Not Found':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND)
def take_update_confirm_dispute_cancel(self, request, format=None): def take_update_confirm_dispute_cancel(self, request, format=None):
''' '''
@ -177,7 +202,7 @@ class OrderView(viewsets.ViewSet):
# 3) If action is cancel # 3) If action is cancel
elif action == 'cancel': elif action == 'cancel':
valid, context = Logics.cancel_order(order,request.user,invoice) valid, context = Logics.cancel_order(order,request.user)
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) if not valid: return Response(context,status.HTTP_400_BAD_REQUEST)
# 4) If action is confirm # 4) If action is confirm
@ -190,11 +215,12 @@ class OrderView(viewsets.ViewSet):
# 6) If action is dispute # 6) If action is dispute
elif action == 'rate' and rating: elif action == 'rate' and rating:
pass valid, context = Logics.rate_counterparty(order,request.user, rating)
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST)
# If nothing... something else is going on. Probably not allowed! # If nothing... something else is going on. Probably not allowed!
else: else:
return Response({'bad_request':'Not allowed'}) return Response({'bad_request':'The Robotic Satoshis working in the warehouse did not understand you'})
return self.get(request) return self.get(request)

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core" import { Paper, Alert, AlertTitle, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core"
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
function getCookie(name) { function getCookie(name) {
@ -104,7 +104,8 @@ export default class MakerPage extends Component {
}; };
fetch("/api/make/",requestOptions) fetch("/api/make/",requestOptions)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => (console.log(data) & this.props.history.push('/order/' + data.id))); .then((data) => (this.setState({badRequest:data.bad_request})
& (data.id ? this.props.history.push('/order/' + data.id) :"")));
} }
render() { render() {
@ -242,6 +243,13 @@ export default class MakerPage extends Component {
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} > <Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >
Create Order Create Order
</Button> </Button>
</Grid>
<Grid item xs={12} align="center">
{this.state.badRequest ?
<Typography component="subtitle2" variant="subtitle2" color="secondary">
{this.state.badRequest} <br/>
</Typography>
: ""}
<Typography component="subtitle2" variant="subtitle2"> <Typography component="subtitle2" variant="subtitle2">
<div align='center'> <div align='center'>
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode} Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode}

View File

@ -68,6 +68,7 @@ export default class OrderPage extends Component {
isBuyer:data.buyer, isBuyer:data.buyer,
isSeller:data.seller, isSeller:data.seller,
expiresAt:data.expires_at, expiresAt:data.expires_at,
badRequest:data.bad_request,
}); });
}); });
} }