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'''
queryset = Order.objects.filter(maker=user)
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)
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
def validate_order_size(order):
'''Checks if order is withing limits at t0'''
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:
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
def take(order, user):
@ -64,11 +64,12 @@ class Logics():
satoshis_now = order.satoshis
else:
# 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()
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
return satoshis_now
return int(satoshis_now)
def order_expires(order):
order.status = Order.Status.EXP
@ -76,6 +77,16 @@ class Logics():
order.taker = None
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
def update_invoice(cls, order, user, invoice):
is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice)
@ -117,12 +128,26 @@ class Logics():
order.save()
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
def cancel_order(cls, order, user, state):
# 1) When maker cancels before bond
# 1) When maker cancels before bond
'''The order never shows up on the book and order
status becomes "cancelled". That's it.'''
if order.status == Order.Status.WFB and order.maker == user:
@ -140,12 +165,12 @@ class Logics():
''' The order goes back to the book as public.
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 the public book if taker cancels.
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.
When a user asks for cancel, 'order.is_pending_cancel' goes True.
When the second user asks for cancel. Order is totally cancelled.
@ -154,22 +179,23 @@ class Logics():
else:
return False, {'bad_request':'You cannot cancel this order'}
@classmethod
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():
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:
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)
bond_satoshis = int(order.satoshis_now * BOND_SIZE)
order.last_satoshis = cls.satoshis_now(order)
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.'
# Gen HODL Invoice
@ -188,22 +214,27 @@ class Logics():
expires_at = expires_at)
order.save()
return True, {'invoice':invoice,'bond_satoshis':bond_satoshis}
return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis}
@classmethod
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.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)):
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.'}
# 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)):
cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond
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:
return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
# Invoice exists, but was already locked or settled
else:
# Return the previous invoice if there was one
return True, {'invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
return False, None
order.satoshis_now = cls.satoshis_now(order)
bond_satoshis = int(order.satoshis_now * BOND_SIZE)
order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE
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.'
# Gen HODL Invoice
@ -222,4 +253,42 @@ class Logics():
expires_at = expires_at)
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):
MAKEBOND = 0, 'Maker bond'
TAKEBOND = 1, 'Taker-buyer bond'
TAKEBOND = 1, 'Taker bond'
TRESCROW = 2, 'Trade escrow'
PAYBUYER = 3, 'Payment to buyer'
class Status(models.IntegerChoices):
INVGEN = 0, 'Hodl invoice was generated'
LOCKED = 1, 'Hodl invoice has HTLCs locked'
SETLED = 2, 'Invoice settled'
RETNED = 3, 'Hodl invoice was returned'
MISSNG = 4, 'Buyer invoice is missing'
VALIDI = 5, 'Buyer invoice is valid'
INFAIL = 6, 'Buyer invoice routing failed'
INVGEN = 0, 'Generated'
LOCKED = 1, 'Locked'
SETLED = 2, 'Settled'
RETNED = 3, 'Returned'
MISSNG = 4, 'Missing'
VALIDI = 5, 'Valid'
INFAIL = 6, 'Failed routing'
# payment use details
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)
def __str__(self):
# Make relational back to ORDER
return (f'HTLC {self.id}: {self.Concepts(self.concept).label}')
return (f'HTLC {self.id}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
class Order(models.Model):
@ -74,16 +73,16 @@ class Order(models.Model):
ETH = 3, 'ETH'
class Status(models.IntegerChoices):
WFB = 0, 'Waiting for maker bond'
PUB = 1, 'Public'
DEL = 2, 'Deleted'
TAK = 3, 'Waiting for taker bond'
UCA = 4, 'Cancelled'
WF2 = 5, 'Waiting for trade collateral and buyer invoice'
WFE = 6, 'Waiting only for seller trade collateral'
WFI = 7, 'Waiting only for buyer invoice'
CHA = 8, 'Sending fiat - In chatroom'
CCA = 9, 'Collaboratively cancelled'
WFB = 0, 'Waiting for maker bond'
PUB = 1, 'Public'
DEL = 2, 'Deleted'
TAK = 3, 'Waiting for taker bond'
UCA = 4, 'Cancelled'
WF2 = 5, 'Waiting for trade collateral and buyer invoice'
WFE = 6, 'Waiting only for seller trade collateral'
WFI = 7, 'Waiting only for buyer invoice'
CHA = 8, 'Sending fiat - In chatroom'
CCA = 9, 'Collaboratively cancelled'
FSE = 10, 'Fiat sent - In chatroom'
FCO = 11, 'Fiat confirmed'
SUC = 12, 'Sucessfully settled'
@ -130,7 +129,7 @@ class Order(models.Model):
def __str__(self):
# 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)
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 .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
from .models import Order
from .models import LNPayment, Order
from .logics import Logics
from .nick_generator.nick_generator import NickGenerator
@ -74,75 +74,100 @@ class OrderView(viewsets.ViewSet):
lookup_url_kwarg = 'order_id'
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)
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)
# check if exactly one order is found in the db
if len(order) == 1 :
order = order[0]
if len(order) != 1 :
return Response({'bad_request':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND)
# This is our order.
order = order[0]
# 1) If order expired
if order.status == Order.Status.EXP:
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
# 1) If order expired
if order.status == Order.Status.EXP:
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
# 2) If order cancelled
if order.status == Order.Status.UCA:
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
if order.status == Order.Status.CCA:
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
# 2) If order cancelled
if order.status == Order.Status.UCA:
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
if order.status == Order.Status.CCA:
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
data = ListOrderSerializer(order).data
data = ListOrderSerializer(order).data
# Add booleans if user is maker, taker, partipant, buyer or seller
data['is_maker'] = order.maker == request.user
data['is_taker'] = order.taker == request.user
data['is_participant'] = data['is_maker'] or data['is_taker']
# 3) If not a participant and order is not public, forbid.
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)
# 4) Non participants can view details (but only if PUB)
elif not data['is_participant'] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK)
# Add booleans if user is maker, taker, partipant, buyer or seller
data['is_maker'] = order.maker == request.user
data['is_taker'] = order.taker == request.user
data['is_participant'] = data['is_maker'] or data['is_taker']
# 3) If not a participant and order is not public, forbid.
if not data['is_participant'] and order.status != Order.Status.PUB:
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)
elif not data['is_participant'] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK)
# For participants add position side, nicks and status as message
data['is_buyer'] = Logics.is_buyer(order,request.user)
data['is_seller'] = Logics.is_seller(order,request.user)
data['maker_nick'] = str(order.maker)
data['taker_nick'] = str(order.taker)
data['status_message'] = Order.Status(order.status).label
# For participants add position side, nicks and status as message
data['is_buyer'] = Logics.is_buyer(order,request.user)
data['is_seller'] = Logics.is_seller(order,request.user)
data['maker_nick'] = str(order.maker)
data['taker_nick'] = str(order.taker)
data['status_message'] = Order.Status(order.status).label
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice.
if order.status == Order.Status.WFB and data['is_maker']:
valid, context = Logics.gen_maker_hodl_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER HODL invoice.
elif order.status == Order.Status.TAK and data['is_taker']:
valid, context = Logics.gen_taker_hodl_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
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.
elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE) and data['is_seller']:
valid, context = Logics.gen_seller_hodl_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice.
if order.status == Order.Status.WFB and data['is_maker']:
valid, context = Logics.gen_maker_hodl_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER HODL invoice.
elif order.status == Order.Status.TAK and data['is_taker']:
valid, context = Logics.gen_taker_hodl_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 7) If status is 'WF2'or'WTC'
elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE):
return Response(data, status.HTTP_200_OK)
return Response({'Order Not Found':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND)
# 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:
data = {**data, **context}
else:
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)
def take_update_confirm_dispute_cancel(self, request, format=None):
'''
@ -177,7 +202,7 @@ class OrderView(viewsets.ViewSet):
# 3) If action is 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)
# 4) If action is confirm
@ -190,11 +215,12 @@ class OrderView(viewsets.ViewSet):
# 6) If action is dispute
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!
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)

View File

@ -1,5 +1,5 @@
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'
function getCookie(name) {
@ -104,7 +104,8 @@ export default class MakerPage extends Component {
};
fetch("/api/make/",requestOptions)
.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() {
@ -242,6 +243,13 @@ export default class MakerPage extends Component {
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >
Create Order
</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">
<div align='center'>
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,
isSeller:data.seller,
expiresAt:data.expires_at,
badRequest:data.bad_request,
});
});
}