mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 19:06:26 +00:00
Merge pull request #20 from Reckless-Satoshi/htlc-model
Htlc model and first logics
This commit is contained in:
commit
8c80301dba
17
.env-sample
Normal file
17
.env-sample
Normal 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'
|
26
api/admin.py
26
api/admin.py
@ -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
55
api/lightning.py
Normal 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
294
api/logics.py
Normal 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}
|
158
api/models.py
158
api/models.py
@ -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())
|
||||
|
||||
|
@ -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)
|
@ -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()),
|
||||
]
|
272
api/views.py
272
api/views.py
@ -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)
|
||||
|
15
dev_utils/reinitiate_db.sh
Normal file
15
dev_utils/reinitiate_db.sh
Normal 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
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
0
frontend/src/components/TradePipelineBox.js
Normal file
0
frontend/src/components/TradePipelineBox.js
Normal 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)=>{
|
||||
|
BIN
frontend/static/assets/misc/unknown_avatar.png
Normal file
BIN
frontend/static/assets/misc/unknown_avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
8
setup.md
8
setup.md
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user