Merge pull request #20 from Reckless-Satoshi/htlc-model

Htlc model and first logics
This commit is contained in:
Reckless_Satoshi 2022-01-07 11:56:10 +00:00 committed by GitHub
commit 8c80301dba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 920 additions and 227 deletions

17
.env-sample Normal file
View File

@ -0,0 +1,17 @@
# Market price public API
MARKET_PRICE_API = 'https://blockchain.info/ticker'
# Trade fee in percentage %
FEE = 0.002
# Bond size in percentage %
BOND_SIZE = 0.01
# Trade limits in satoshis
MIN_TRADE = 10000
MAX_TRADE = 500000
# Expiration time in minutes
EXPIRATION_MAKE = 5
# Username for HTLCs escrows
ESCROW_USERNAME = 'admin'

View File

@ -1,8 +1,8 @@
from django.contrib import admin
from django.db import models
from django_admin_relation_links import AdminChangeLinksMixin
from django.contrib.auth.models import Group, User
from django.contrib.auth.admin import UserAdmin
from .models import Order, Profile
from .models import Order, LNPayment, Profile
admin.site.unregister(Group)
admin.site.unregister(User)
@ -23,14 +23,20 @@ class EUserAdmin(UserAdmin):
return obj.profile.avatar_tag()
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at')
list_display_links = ('maker','taker')
pass
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
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','num_satoshis','type','invoice','expires_at','sender_link','receiver_link')
list_display_links = ('id','concept')
change_links = ('sender','receiver')
@admin.register(Profile)
class UserProfileAdmin(admin.ModelAdmin):
list_display = ('avatar_tag','user','id','total_ratings','avg_rating','num_disputes','lost_disputes')
list_display_links =['user']
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes')
list_display_links = ('avatar_tag','id')
change_links =['user']
readonly_fields = ['avatar_tag']
pass

55
api/lightning.py Normal file
View File

@ -0,0 +1,55 @@
from datetime import timedelta
from django.utils import timezone
import random
import string
#######
# Placeholder functions
# Should work with LND (maybe c-lightning in the future)
class LNNode():
'''
Place holder functions to interact with Lightning Node
'''
def gen_hodl_invoice(num_satoshis, description, expiry):
'''Generates hodl invoice to publish an order'''
# 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): # 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'''
return True
def charge_hodl_htlcs(invoice):
'''Charges a LN hodl invoice'''
return True
def free_hodl_htlcs(invoice):
'''Returns sats'''
return True

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.Currencies(order.currency).label]['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,16 +3,63 @@ 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
#############################
# TODO
# Load hparams from .env file
min_satoshis_trade = 10*1000
max_satoshis_trade = 500*1000
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):
class Types(models.IntegerChoices):
NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hodl)
HODL = 1, 'Hodl invoice'
class Concepts(models.IntegerChoices):
MAKEBOND = 0, 'Maker bond'
TAKEBOND = 1, 'Taker bond'
TRESCROW = 2, 'Trade escrow'
PAYBUYER = 3, 'Payment to buyer'
class Status(models.IntegerChoices):
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)
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 info
invoice = 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()
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
# 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):
return (f'HTLC {self.id}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
class Order(models.Model):
@ -25,30 +72,29 @@ class Order(models.Model):
EUR = 2, 'EUR'
ETH = 3, 'ETH'
class Status(models.TextChoices):
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'
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'
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'
# order info, id = models.CharField(max_length=64, unique=True, null=False)
status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB)
# order info
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
@ -56,42 +102,60 @@ class Order(models.Model):
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
currency = models.PositiveSmallIntegerField(choices=Currencies.choices, null=False)
amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)])
payment_method = models.CharField(max_length=30, null=False, default="Not specified")
premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)])
satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(min_satoshis_trade), MaxValueValidator(max_satoshis_trade)])
is_explicit = models.BooleanField(default=False, null=False) # pricing method. A explicit amount of sats, or a relative premium above/below market.
payment_method = models.CharField(max_length=30, null=False, default="not specified", blank=True)
# 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 market
premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], 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) # unique = True, a taker can only take 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
has_maker_bond = models.BooleanField(default=False, null=False)
has_taker_bond = models.BooleanField(default=False, null=False)
has_trade_collat = models.BooleanField(default=False, null=False)
maker_bond_secret = models.CharField(max_length=300, unique=False, null=True, default=None)
taker_bond_secret = models.CharField(max_length=300, unique=False, null=True, default=None)
trade_collat_secret = models.CharField(max_length=300, unique=False, null=True, default=None)
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
# buyer payment LN invoice
has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid
invoice = models.CharField(max_length=300, unique=False, null=True, default=None)
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):
user = models.OneToOneField(User,on_delete=models.CASCADE)
# Ratings stored as a comma separated integer list
total_ratings = models.PositiveIntegerField(null=False, default=0)
latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list]) # Will only store latest ratings
avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)])
latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store latest ratings
avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True)
# Disputes
num_disputes = models.PositiveIntegerField(null=False, default=0)
lost_disputes = models.PositiveIntegerField(null=False, default=0)
# RoboHash
avatar = models.ImageField(default="static/assets/avatars/unknown.png", verbose_name='Avatar')
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
@ -102,15 +166,21 @@ class Profile(models.Model):
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
@receiver(pre_delete, sender=User)
def del_avatar_from_disk(sender, instance, **kwargs):
avatar_file=Path('frontend/' + instance.profile.avatar.url)
avatar_file.unlink() # FIX deleting user fails if avatar is not found
def __str__(self):
return self.user.username
# to display avatars in admin panel
def get_avatar(self):
if not self.avatar:
return 'static/assets/avatars/unknown.png'
return 'static/assets/misc/unknown_avatar.png'
return self.avatar.url
# 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

@ -1,7 +1,7 @@
from rest_framework import serializers
from .models import Order
class OrderSerializer(serializers.ModelSerializer):
class ListOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker')
@ -10,3 +10,8 @@ class MakeOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis')
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

@ -1,9 +1,9 @@
from django.urls import path
from .views import MakeOrder, OrderView, UserGenerator, BookView
from .views import OrderMakerView, OrderView, UserView, BookView
urlpatterns = [
path('make/', MakeOrder.as_view()),
path('order/', OrderView.as_view()),
path('usergen/', UserGenerator.as_view()),
path('make/', OrderMakerView.as_view()),
path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})),
path('usergen/', UserView.as_view()),
path('book/', BookView.as_view()),
]

View File

@ -1,12 +1,14 @@
from rest_framework import serializers, status
from rest_framework import status, viewsets
from rest_framework.generics import CreateAPIView, ListAPIView
from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.conf.urls.static import static
from .serializers import OrderSerializer, MakeOrderSerializer
from .models import Order
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
from .models import LNPayment, Order
from .logics import Logics
from .nick_generator.nick_generator import NickGenerator
from robohash import Robohash
@ -17,94 +19,212 @@ import hashlib
from pathlib import Path
from datetime import timedelta
from django.utils import timezone
from decouple import config
# .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)
# Create your views here.
class MakeOrder(APIView):
class OrderMakerView(CreateAPIView):
serializer_class = MakeOrderSerializer
def post(self,request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
otype = serializer.data.get('type')
currency = serializer.data.get('currency')
amount = serializer.data.get('amount')
payment_method = serializer.data.get('payment_method')
premium = serializer.data.get('premium')
satoshis = serializer.data.get('satoshis')
is_explicit = serializer.data.get('is_explicit')
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
# query if the user is already a maker or taker, return error
queryset = Order.objects.filter(maker=request.user.id)
if queryset.exists():
return 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 Response({'Bad Request':'You are already taker of an order'},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')
premium = serializer.data.get('premium')
satoshis = serializer.data.get('satoshis')
is_explicit = serializer.data.get('is_explicit')
# Creates a new order in db
order = Order(
type=otype,
currency=currency,
amount=amount,
payment_method=payment_method,
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
expires_at= timezone.now()+timedelta(hours=expiration_time),
maker=request.user)
order.save()
valid, context = Logics.validate_already_maker_or_taker(request.user)
if not valid: return Response(context, status.HTTP_409_CONFLICT)
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
# Creates a new order
order = Order(
type=type,
currency=currency,
amount=amount,
payment_method=payment_method,
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
expires_at=timezone.now()+timedelta(minutes=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
maker=request.user)
return Response(OrderSerializer(order).data, status=status.HTTP_201_CREATED)
# 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()
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
class OrderView(APIView):
serializer_class = OrderSerializer
class OrderView(viewsets.ViewSet):
serializer_class = UpdateOrderSerializer
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:
order = Order.objects.filter(id=order_id)
if order_id == None:
return Response({'bad_request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
# check if exactly one order is found in the db
if len(order) == 1 :
order = order[0]
data = self.serializer_class(order).data
nickname = request.user.username
order = Order.objects.filter(id=order_id)
# Check if requester is participant in the order and add boolean to response
data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname)
# check if exactly one order is found in the db
if len(order) != 1 :
return Response({'bad_request':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND)
#To do fix: data['status_message'] = Order.Status.get(order.status).label
data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value.
# This is our order.
order = order[0]
data['maker_nick'] = str(order.maker)
data['taker_nick'] = str(order.taker)
# 1) If order expired
if order.status == Order.Status.EXP:
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
if data['is_participant']:
return Response(data, status=status.HTTP_200_OK)
else:
# Non participants should not see the status or who is the taker
data.pop('status','status_message','taker','taker_nick')
return Response(data, status=status.HTTP_200_OK)
# 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)
return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND)
data = ListOrderSerializer(order).data
return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
# 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']
class UserGenerator(APIView):
# 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
# 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):
# 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)
# 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')
# 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)
Logics.take(order, request.user)
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
# 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)
# 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)
# 4) If action is confirm
elif action == 'confirm':
pass
# 5) If action is dispute
elif action == 'dispute':
pass
# 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':'The Robotic Satoshis working in the warehouse did not understand you'})
return self.get(request)
class UserView(APIView):
lookup_url_kwarg = 'token'
NickGen = NickGenerator(
lang='English',
@ -113,6 +233,7 @@ class UserGenerator(APIView):
use_noun=True,
max_num=999)
# Probably should be turned into a post method
def get(self,request, format=None):
'''
Get a new user derived from a high entropy token
@ -128,8 +249,7 @@ class UserGenerator(APIView):
value, counts = np.unique(list(token), return_counts=True)
shannon_entropy = entropy(counts, base=62)
bits_entropy = log2(len(value)**len(token))
# Start preparing payload
# Payload
context = {'token_shannon_entropy': shannon_entropy, 'token_bits_entropy': bits_entropy}
# Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity
@ -140,11 +260,11 @@ class UserGenerator(APIView):
# Hashes the token, only 1 iteration. Maybe more is better.
hash = hashlib.sha256(str.encode(token)).hexdigest()
# generate nickname
# Generate nickname
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
context['nickname'] = nickname
# generate avatar
# Generate avatar
rh = Robohash(hash)
rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
@ -155,7 +275,7 @@ class UserGenerator(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)
@ -180,40 +300,40 @@ class UserGenerator(APIView):
def delete(self,request):
user = User.objects.get(id = request.user.id)
# TO DO. Pressing give me another will delete the logged in user
# TO DO. Pressing "give me another" deletes the logged in user
# However it might be a long time recovered user
# Only delete if user live is < 5 minutes
# TODO check if user exists AND it is not a maker or taker!
if user is not None:
avatar_file = avatar_path.joinpath(str(request.user)+".png")
avatar_file.unlink() # Unsafe if avatar does not exist.
logout(request)
user.delete()
return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_301_MOVED_PERMANENTLY)
return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_302_FOUND)
return Response(status=status.HTTP_403_FORBIDDEN)
class BookView(APIView):
serializer_class = OrderSerializer
class BookView(ListAPIView):
serializer_class = ListOrderSerializer
def get(self,request, format=None):
currency = request.GET.get('currency')
type = request.GET.get('type')
queryset = Order.objects.filter(currency=currency, type=type, status=0) # TODO status = 1 for orders that are Public
queryset = Order.objects.filter(currency=currency, type=type, status=int(Order.Status.PUB))
if len(queryset)== 0:
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
queryset = queryset.order_by('created_at')
book_data = []
for order in queryset:
data = OrderSerializer(order).data
data = ListOrderSerializer(order).data
user = User.objects.filter(id=data['maker'])
if len(user) == 1:
data['maker_nick'] = user[0].username
# TODO avoid sending status and takers for book views
#data.pop('status','taker')
# Non participants should not see the status or who is the taker
for key in ('status','taker'):
del data[key]
book_data.append(data)
return Response(book_data, status=status.HTTP_200_OK)

View File

@ -0,0 +1,15 @@
#!/bin/bash
rm db.sqlite3
rm -R api/migrations
rm -R frontend/migrations
rm -R frontend/static/assets/avatars
python3 manage.py makemigrations
python3 manage.py makemigrations api
python3 manage.py migrate
python3 manage.py createsuperuser
python3 manage.py runserver

View File

@ -1,5 +1,6 @@
import React, { Component } from "react";
import { Paper, Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, Link, RouterLink, ListItemAvatar} from "@material-ui/core"
import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@material-ui/core"
import { Link } from 'react-router-dom'
export default class BookPage extends Component {
constructor(props) {
@ -13,18 +14,20 @@ 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)
.then((response) => response.json())
.then((data) => //console.log(data));
this.setState({orders: data}));
this.setState({
orders: data,
not_found: data.not_found,
}));
}
handleCardClick=(e)=>{
console.log(e.target)
this.props.history.push('/order/' + e.target);
console.log(e)
this.props.history.push('/order/' + e);
}
// Make these two functions sequential. getOrderDetails needs setState to be finish beforehand.
@ -53,6 +56,63 @@ export default class BookPage extends Component {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
bookCards=()=>{
return (this.state.orders.map((order) =>
<Grid container item sm={4}>
<Card elevation={6} sx={{ width: 945 }}>
<CardActionArea value={order.id} onClick={() => this.handleCardClick(order.id)}>
<CardContent>
<List dense="true">
<ListItem >
<ListItemAvatar >
<Avatar
alt={order.maker_nick}
src={window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png'}
/>
</ListItemAvatar>
<ListItemText>
<Typography gutterBottom variant="h6">
{order.maker_nick}
</Typography>
</ListItemText>
</ListItem>
{/* CARD PARAGRAPH CONTENT */}
<ListItemText>
<Typography variant="subtitle1" color="text.secondary">
{order.type == 0 ? <b> Buys </b>: <b> Sells </b>}
<b>{parseFloat(parseFloat(order.amount).toFixed(4))}
{" " +this.getCurrencyCode(order.currency)}</b> <a> worth of bitcoin</a>
</Typography>
<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 variant="subtitle1" color="text.secondary">
<b>{" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC</b> (Binance API)
</Typography>
</ListItemText>
</List>
</CardContent>
</CardActionArea>
</Card>
</Grid>
));
}
render() {
return (
<Grid className='orderBook' container spacing={1}>
@ -102,66 +162,30 @@ export default class BookPage extends Component {
</Select>
</FormControl>
</Grid>
{this.state.orders.map((order) =>
<Grid container item sm={4}>
<Card elevation={6} sx={{ width: 945 }}>
{/* To fix! does not pass order.id to handleCardCLick. Instead passes the clicked </>*/}
<CardActionArea value={order.id} onClick={this.handleCardClick}>
<CardContent>
<List dense="true">
<ListItem >
<ListItemAvatar >
<Avatar
alt={order.maker_nick}
src={window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png'}
/>
</ListItemAvatar>
<ListItemText>
<Typography gutterBottom variant="h6">
{order.maker_nick}
</Typography>
</ListItemText>
</ListItem>
{/* CARD PARAGRAPH CONTENT */}
<ListItemText>
<Typography variant="subtitle1" color="text.secondary">
{order.type == 0 ? <b> Buys </b>: <b> Sells </b>}
<b>{parseFloat(parseFloat(order.amount).toFixed(4))}
{" " +this.getCurrencyCode(order.currency)}</b> <a> worth of bitcoin</a>
</Typography>
<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 variant="subtitle1" color="text.secondary">
<b>{" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC</b> (Binance API)
</Typography>
</ListItemText>
</List>
</CardContent>
</CardActionArea>
</Card>
</Grid>
)}
{ this.state.not_found ? "" :
<Grid item xs={12} align="center">
<Typography component="h5" variant="h5">
You are {this.state.type == 0 ? " selling " : " buying "} BTC for {this.state.currencyCode}
</Typography>
</Grid>
}
{ this.state.not_found ?
(<Grid item xs={12} align="center">
<Grid item xs={12} align="center">
<Typography component="h5" variant="h5">
No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode}
</Typography>
</Grid>
<Grid item>
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
</Grid>
<Typography component="body1" variant="body1">
Be the first one to create an order
</Typography>
</Grid>)
: this.bookCards()
}
<Grid item xs={12} align="center">
<Button color="secondary" variant="contained" to="/" component={Link}>
Back

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) {
@ -91,7 +91,7 @@ export default class MakerPage extends Component {
console.log(this.state)
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken},
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
body: JSON.stringify({
type: this.state.type,
currency: this.state.currency,
@ -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

@ -2,6 +2,34 @@ import React, { Component } from "react";
import { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core"
import { Link } from 'react-router-dom'
function msToTime(duration) {
var seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60),
hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return hours + "h " + minutes + "m " + seconds + "s";
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
// pretty numbers
function pn(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@ -26,7 +54,7 @@ export default class OrderPage extends Component {
statusText: data.status_message,
type: data.type,
currency: data.currency,
currencyCode: (data.currency== 1 ) ? "USD": ((data.currency == 2 ) ? "EUR":"ETH"),
currencyCode: this.getCurrencyCode(data.currency),
amount: data.amount,
paymentMethod: data.payment_method,
isExplicit: data.is_explicit,
@ -37,14 +65,39 @@ export default class OrderPage extends Component {
makerNick: data.maker_nick,
takerId: data.taker,
takerNick: data.taker_nick,
isBuyer:data.buyer,
isSeller:data.seller,
expiresAt:data.expires_at,
badRequest:data.bad_request,
});
});
}
// Gets currency code (3 letters) from numeric (e.g., 1 -> USD)
// Improve this function so currencies are read from json
getCurrencyCode(val){
return (val == 1 ) ? "USD": ((val == 2 ) ? "EUR":"ETH")
}
// Fix to use proper react props
handleClickBackButton=()=>{
window.history.back();
}
handleClickTakeOrderButton=()=>{
console.log(this.state)
const requestOptions = {
method: 'POST',
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())
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
}
render (){
return (
<Grid container spacing={1}>
@ -53,17 +106,43 @@ export default class OrderPage extends Component {
BTC {this.state.type ? " Sell " : " Buy "} Order
</Typography>
<Paper elevation={12} style={{ padding: 8,}}>
<List component="nav" aria-label="mailbox folders">
<ListItem>
<List dense="true">
<ListItem >
<ListItemAvatar sx={{ width: 56, height: 56 }}>
<Avatar
alt={this.state.makerNick}
src={window.location.origin +'/static/assets/avatars/' + this.state.makerNick + '.png'}
/>
</ListItemAvatar>
<ListItemText primary={this.state.makerNick} secondary="Order maker" />
<ListItemText primary={this.state.makerNick} secondary="Order maker" align="right"/>
</ListItem>
<Divider />
{this.state.isParticipant ?
<>
{this.state.takerNick!='None' ?
<>
<ListItem align="left">
<ListItemText primary={this.state.takerNick} secondary="Order taker"/>
<ListItemAvatar >
<Avatar
alt={this.state.makerNick}
src={window.location.origin +'/static/assets/avatars/' + this.state.takerNick + '.png'}
/>
</ListItemAvatar>
</ListItem>
<Divider />
</>:
""
}
<ListItem>
<ListItemText primary={this.state.statusText} secondary="Order status"/>
</ListItem>
<Divider />
</>
:""
}
<ListItem>
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))+" "+this.state.currencyCode} secondary="Amount and currency requested"/>
</ListItem>
@ -80,32 +159,26 @@ export default class OrderPage extends Component {
}
</ListItem>
<Divider />
{this.state.isParticipant ?
<>
<ListItem>
<ListItemText primary={this.state.statusText} secondary="Order status"/>
</ListItem>
<Divider />
{ this.state.takerNick!='None' ?
<><ListItem>
<ListItemText primary={this.state.takerNick} secondary="Order taker"/>
</ListItem>
<Divider /> </>: ""}
</>
:""
}
<ListItem>
<ListItemText primary={'#'+this.orderId} secondary="Order ID"/>
</ListItem>
<Divider />
<ListItem>
<ListItemText primary={msToTime( new Date(this.state.expiresAt) - Date.now())} secondary="Expires in "/>
</ListItem>
</List>
</Paper>
<Grid item xs={12} align="center">
{this.state.isParticipant ? "" : <Button variant='contained' color='primary' to='/home' component={Link}>Take Order</Button>}
{this.state.isParticipant ? "" : <Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>}
</Grid>
<Grid item xs={12} align="center">
{this.state.isParticipant ? "" : <Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>}
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
</Grid>
</Paper>
</Grid>
</Grid>
);

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())
@ -74,7 +74,7 @@ export default class UserGenPage extends Component {
this.setState({
token: this.genBase62Token(32),
})
this.getGeneratedUser();
this.reload_for_csrf_to_work();
}
handleChangeToken=(e)=>{

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

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
@ -28,6 +31,9 @@ source /usr/local/bin/virtualenvwrapper.sh
### Install Django and Restframework
`pip3 install django djangorestframework`
## Install Django admin relational links
`pip install django-admin-relation-links`
*Django 4.0 at the time of writting*
### Launch the local development node