merge htlc-model

This commit is contained in:
LowEntropyFace 2022-01-08 06:51:55 -05:00
parent 4516f1974a
commit 215af668a2
12 changed files with 601 additions and 191 deletions

View File

@ -24,13 +24,13 @@ class EUserAdmin(UserAdmin):
@admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('id','type','maker_link','taker_link','status','amount','currency','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
list_display_links = ('id','type')
change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow')
@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','expires_at','sender_link','receiver_link')
list_display_links = ('id','concept')
change_links = ('sender','receiver')

View File

@ -1,3 +1,6 @@
from datetime import timedelta
from django.utils import timezone
import random
import string
@ -10,17 +13,28 @@ class LNNode():
Place holder functions to interact with Lightning Node
'''
def gen_hodl_invoice():
def gen_hodl_invoice(num_satoshis, description, expiry):
'''Generates hodl invoice to publish an order'''
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=80))
# TODO
invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX
payment_hash = ''.join(random.choices(string.ascii_uppercase + string.digits, k=40)) #FIX
expires_at = timezone.now() + timedelta(hours=8) ##FIX
return invoice, payment_hash, expires_at
def validate_hodl_invoice_locked():
'''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'''

294
api/logics.py Normal file
View File

@ -0,0 +1,294 @@
from datetime import timedelta
from django.utils import timezone
import requests
from .lightning import LNNode
from .models import Order, LNPayment, User
from decouple import config
FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE'))
MARKET_PRICE_API = config('MARKET_PRICE_API')
ESCROW_USERNAME = config('ESCROW_USERNAME')
MIN_TRADE = int(config('MIN_TRADE'))
MAX_TRADE = int(config('MAX_TRADE'))
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE'))
EXP_TRADE_ESCR_INVOICE = int(config('EXP_TRADE_ESCR_INVOICE'))
BOND_EXPIRY = int(config('BOND_EXPIRY'))
ESCROW_EXPIRY = int(config('ESCROW_EXPIRY'))
class Logics():
# escrow_user = User.objects.get(username=ESCROW_USERNAME)
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 validate_order_size(order):
'''Checks if order is withing limits at t0'''
if order.t0_satoshis > 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': 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):
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)
def satoshis_now(order):
''' checks trade amount in sats '''
if order.is_explicit:
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.currency_dict[str(order.currency)]]['last'])
satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000
return int(satoshis_now)
def order_expires(order):
order.status = Order.Status.EXP
order.maker = None
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)
# only user is the buyer and a valid LN invoice
if not (cls.is_buyer(order, user) or is_valid_invoice):
return False, {'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}
order.buyer_invoice, _ = LNPayment.objects.update_or_create(
concept = LNPayment.Concepts.PAYBUYER,
type = LNPayment.Types.NORM,
sender = User.objects.get(username=ESCROW_USERNAME),
receiver= user,
# if there is a LNPayment matching these above, it updates that one 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 is 'Waiting for invoice'. Move forward to 'waiting for invoice'
if order.status == Order.Status.WFE: order.status = Order.Status.CHA
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' or to 'chat'
if order.status == Order.Status.WF2:
print(order.trade_escrow)
if order.trade_escrow:
if order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA
else:
order.status = Order.Status.WFE
# If the order status was Payment Failed. Move forward to invoice Updated.
if order.status == Order.Status.FAI:
order.status = Order.Status.UPI
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
'''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:
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
# 2) When maker cancels after bond
'''The order dissapears from book and goes to cancelled.
Maker is charged a small amount of sats, to prevent DDOS
on the LN node and order book'''
# 3) When taker cancels before bond
''' The order goes back to the book as public.
LNPayment "order.taker_bond" is deleted() '''
# 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 (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.
Has a small cost for both parties to prevent node DDOS.'''
else:
return False, {'bad_request':'You cannot cancel this order'}
@classmethod
def gen_maker_hodl_invoice(cls, order, user):
# 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, {'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 and is still unpaid
if order.maker_bond:
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.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
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
order.maker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.MAKEBOND,
type = LNPayment.Types.HODL,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
status = LNPayment.Status.INVGEN,
num_satoshis = bond_satoshis,
description = description,
payment_hash = payment_hash,
expires_at = expires_at)
order.save()
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 X minutes and unpaid still
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)):
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 False, None
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
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
order.taker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.TAKEBOND,
type = LNPayment.Types.HODL,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
status = LNPayment.Status.INVGEN,
num_satoshis = bond_satoshis,
description = description,
payment_hash = payment_hash,
expires_at = expires_at)
order.save()
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

@ -3,9 +3,9 @@ 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 decouple import config
from pathlib import Path
import json
@ -13,10 +13,11 @@ import json
# TODO
# Load hparams from .env file
MIN_TRADE = 10*1000 #In sats
MAX_TRADE = 500*1000
FEE = 0.002 # Trade fee in %
BOND_SIZE = 0.01 # Bond in %
MIN_TRADE = int(config('MIN_TRADE'))
MAX_TRADE = int(config('MAX_TRADE'))
FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE'))
class LNPayment(models.Model):
@ -27,38 +28,39 @@ 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'
CHRGED = 2, 'Hodl invoice was charged'
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'
INVGEN = 0, 'Generated'
LOCKED = 1, 'Locked'
SETLED = 2, 'Settled'
RETNED = 3, 'Returned'
MISSNG = 4, 'Missing'
VALIDI = 5, 'Valid'
INFAIL = 6, 'Failed routing'
# 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)
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)
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):
@ -67,26 +69,25 @@ class Order(models.Model):
SELL = 1, 'SELL'
class Status(models.IntegerChoices):
WFB = 0, 'Waiting for bond'
PUB = 1, 'Published in order book'
DEL = 2, 'Deleted from order book'
TAK = 3, 'Taken'
UCA = 4, 'Unilaterally cancelled'
RET = 5, 'Returned to order book' # Probably same as 1 in most cases.
WF2 = 6, 'Waiting for trade collateral and buyer invoice'
WTC = 7, 'Waiting only for trade collateral'
WBI = 8, 'Waiting only for buyer invoice'
EXF = 9, 'Exchanging fiat / In chat'
CCA = 10, 'Collaboratively cancelled'
FSE = 11, 'Fiat sent'
FCO = 12, 'Fiat confirmed'
SUC = 13, 'Sucessfully settled'
FAI = 14, 'Failed lightning network routing'
UPI = 15, 'Updated invoice'
DIS = 16, 'In dispute'
MLD = 17, 'Maker lost dispute'
TLD = 18, 'Taker lost dispute'
EXP = 19, 'Expired'
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'
FAI = 13, 'Failed lightning network routing'
UPI = 14, 'Updated invoice'
DIS = 15, 'In dispute'
MLD = 16, 'Maker lost dispute'
TLD = 17, 'Taker lost dispute'
EXP = 18, 'Expired'
currency_dict = json.load(open('./api/currencies.json'))
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
@ -104,15 +105,18 @@ class Order(models.Model):
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
is_explicit = models.BooleanField(default=False, null=False)
# marked to marked
# marked to market
premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True)
# explicit
satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True)
# how many sats at creation and at last check (relevant for marked to market)
t0_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # sats at creation
last_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE*2)], blank=True) # sats last time checked. Weird if 2* trade max...
# order participants
maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order
taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order
is_pending_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
# order collateral
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
@ -122,6 +126,19 @@ 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)
def __str__(self):
# Make relational back to ORDER
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):
to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow)
for htlc in to_delete:
try:
htlc.delete()
except:
pass
class Profile(models.Model):
@ -165,3 +182,4 @@ 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())

View File

@ -11,7 +11,7 @@ class MakeOrderSerializer(serializers.ModelSerializer):
model = Order
fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis')
class UpdateOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('id','buyer_invoice')
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=300, allow_null=True, allow_blank=True, default=None)
action = serializers.ChoiceField(choices=('take','update_invoice','dispute','cancel','confirm','rate'), allow_null=False)
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)

View File

@ -3,7 +3,7 @@ from .views import OrderMakerView, OrderView, UserView, BookView, get_currencies
urlpatterns = [
path('make/', OrderMakerView.as_view()),
path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})),
path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})),
path('usergen/', UserView.as_view()),
path('book/', BookView.as_view()),
path('currencies/', get_currencies_json),

View File

@ -1,15 +1,14 @@
from rest_framework import status, serializers
from rest_framework import status, viewsets
from rest_framework.generics import CreateAPIView, ListAPIView
from rest_framework.views import APIView
from rest_framework import viewsets
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 .lightning import LNNode
from .models import LNPayment, Order
from .logics import Logics
from .nick_generator.nick_generator import NickGenerator
from robohash import Robohash
@ -20,29 +19,16 @@ import hashlib
from pathlib import Path
from datetime import timedelta
from django.utils import timezone
from decouple import config
import json
from django.http import HttpResponse
# .env
expiration_time = 8
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
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):
@ -51,8 +37,9 @@ class OrderMakerView(CreateAPIView):
def post(self,request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
otype = serializer.data.get('type')
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
type = serializer.data.get('type')
currency = serializer.data.get('currency')
amount = serializer.data.get('amount')
payment_method = serializer.data.get('payment_method')
@ -60,27 +47,28 @@ class OrderMakerView(CreateAPIView):
satoshis = serializer.data.get('satoshis')
is_explicit = serializer.data.get('is_explicit')
valid, response = validate_already_maker_or_taker(request)
if not valid:
return response
valid, context = Logics.validate_already_maker_or_taker(request.user)
if not valid: return Response(context, status.HTTP_409_CONFLICT)
# Creates a new order in db
# Creates a new order
order = Order(
type=otype,
status=Order.Status.PUB, # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond)
type=type,
currency=currency,
amount=amount,
payment_method=payment_method,
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
expires_at= timezone.now()+timedelta(hours=expiration_time),
expires_at=timezone.now()+timedelta(minutes=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
maker=request.user)
# TODO move to Order class method when new instance is created!
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
valid, context = Logics.validate_order_size(order)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
order.save()
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
@ -89,87 +77,154 @@ 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:
if order_id == None:
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 :
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)
# 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
nickname = request.user.username
# Add booleans if user is maker, taker, partipant, buyer or seller
data['is_maker'] = str(order.maker) == nickname
data['is_taker'] = str(order.taker) == nickname
data['is_maker'] = order.maker == request.user
data['is_taker'] = order.taker == request.user
data['is_participant'] = data['is_maker'] or data['is_taker']
data['is_buyer'] = (data['is_maker'] and order.type == Order.Types.BUY) or (data['is_taker'] and order.type == Order.Types.SELL)
data['is_seller'] = (data['is_maker'] and order.type == Order.Types.SELL) or (data['is_taker'] and order.type == Order.Types.BUY)
# 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:
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)
# return nicks too
# 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
if data['is_participant']:
return Response(data, status=status.HTTP_200_OK)
# 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:
# Non participants should not see the status, who is the taker, etc
for key in ('status','status_message','taker','taker_nick','is_maker','is_taker','is_buyer','is_seller'):
del data[key]
return Response(data, status=status.HTTP_200_OK)
return Response(context, status.HTTP_400_BAD_REQUEST)
return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND)
return Response({'Bad Request':'Order ID parameter not found in request'}, status=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):
def take_or_update(self, request, format=None):
# 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):
'''
Here take place all of the user updates to the order object.
That is: take, confim, cancel, dispute, update_invoice or rate.
'''
order_id = request.GET.get(self.lookup_url_kwarg)
serializer = UpdateOrderSerializer(data=request.data)
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
order = Order.objects.get(id=order_id)
if serializer.is_valid():
invoice = serializer.data.get('buyer_invoice')
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' 6)'rate' (counterparty)
action = serializer.data.get('action')
invoice = serializer.data.get('invoice')
rating = serializer.data.get('rating')
# If this is an empty POST request (no invoice), it must be taker request!
if not invoice and order.status == Order.Status.PUB:
# 1) If action is take, it is be taker request!
if action == 'take':
if 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)
if not valid:
return response
Logics.take(order, request.user)
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
order.taker = self.request.user
order.status = Order.Status.TAK
# 2) If action is update (invoice)
elif action == 'update_invoice' and invoice:
valid, context = Logics.update_invoice(order,request.user,invoice)
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST)
#TODO REPLY WITH HODL INVOICE
data = ListOrderSerializer(order).data
# 3) If action is cancel
elif action == 'cancel':
valid, context = Logics.cancel_order(order,request.user)
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST)
# An invoice came in! update it
elif invoice:
if LNNode.validate_ln_invoice(invoice):
order.invoice = invoice
# 4) If action is confirm
elif action == 'confirm':
pass
#TODO Validate if request comes from PARTICIPANT AND BUYER
# 5) If action is dispute
elif action == 'dispute':
pass
#If the order status was Payment Failed. Move foward to invoice Updated.
if order.status == Order.Status.FAI:
order.status = Order.Status.UPI
# 6) If action is dispute
elif action == 'rate' and rating:
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':'Invalid Lightning Network Invoice. It starts by LNTB...'})
return Response({'bad_request':'The Robotic Satoshis working in the warehouse did not understand you'})
# Something else is going on. Probably not allowed.
else:
return Response({'bad_request':'Not allowed'})
order.save()
return self.get(request)
class UserView(APIView):
@ -223,7 +278,7 @@ class UserView(APIView):
with open(image_path, "wb") as f:
rh.img.save(f, format="png")
# Create new credentials and logsin if nickname is new
# Create new credentials and log in if nickname is new
if len(User.objects.filter(username=nickname)) == 0:
User.objects.create_user(username=nickname, password=token, is_staff=False)
user = authenticate(request, username=nickname, password=token)
@ -279,6 +334,7 @@ class BookView(ListAPIView):
user = User.objects.filter(id=data['maker'])
if len(user) == 1:
data['maker_nick'] = user[0].username
# Non participants should not see the status or who is the taker
for key in ('status','taker'):
del data[key]

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) {
@ -15,7 +16,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)
@ -26,15 +26,6 @@ export default class BookPage extends Component {
not_found: data.not_found,
}));
}
getCurrencyDict() {
fetch('/api/currencies')
.then((response) => response.json())
.then((data) =>
this.setState({
currencies_dict: data
}));
}
handleCardClick=(e)=>{
console.log(e)
@ -56,6 +47,14 @@ export default class BookPage extends Component {
this.getOrderDetails();
}
getCurrencyDict() {
fetch('/api/currencies')
.then((response) => response.json())
.then((data) =>
this.setState({
currencies_dict: data
}));
}
// Gets currency code (3 letters) from numeric (e.g., 1 -> USD)
// Improve this function so currencies are read from json
getCurrencyCode(val){
@ -101,14 +100,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)
@ -188,13 +187,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>
<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>
<Typography component="body1" variant="body1">
Be the first one to create an order
</Typography>
</Grid>)
: this.bookCards()
}

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) {
@ -37,7 +37,9 @@ export default class MakerPage extends Component {
payment_method: this.defaultPaymentMethod,
premium: 0,
satoshis: null,
currencies_dict: {"1":"USD"}
}
this.getCurrencyDict()
}
handleTypeChange=(e)=>{
@ -46,10 +48,9 @@ export default class MakerPage extends Component {
});
}
handleCurrencyChange=(e)=>{
var code = (e.target.value == 1 ) ? "USD": ((e.target.value == 2 ) ? "EUR":"ETH")
this.setState({
currency: e.target.value,
currencyCode: code,
currencyCode: this.getCurrencyCode(e.target.value),
});
}
handleAmountChange=(e)=>{
@ -104,7 +105,22 @@ 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) :"")));
}
getCurrencyDict() {
fetch('/api/currencies')
.then((response) => response.json())
.then((data) =>
this.setState({
currencies_dict: data
}));
}
getCurrencyCode(val){
return this.state.currencies_dict[val.toString()]
}
render() {
@ -242,6 +258,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,
});
});
}
@ -87,8 +88,10 @@ export default class OrderPage extends Component {
console.log(this.state)
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
body: JSON.stringify({}),
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'take',
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json())

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

View File

@ -4,7 +4,10 @@
`sudo apt install python3 python3 pip`
### Install virtual environments
`pip install virtualenvwrapper`
```
pip install virtualenvwrapper
pip install python-decouple
```
### Add to .bashrc