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.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.models import Group, User
|
||||||
from django.contrib.auth.admin import UserAdmin
|
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(Group)
|
||||||
admin.site.unregister(User)
|
admin.site.unregister(User)
|
||||||
@ -23,14 +23,20 @@ class EUserAdmin(UserAdmin):
|
|||||||
return obj.profile.avatar_tag()
|
return obj.profile.avatar_tag()
|
||||||
|
|
||||||
@admin.register(Order)
|
@admin.register(Order)
|
||||||
class OrderAdmin(admin.ModelAdmin):
|
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at')
|
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 = ('maker','taker')
|
list_display_links = ('id','type')
|
||||||
pass
|
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)
|
@admin.register(Profile)
|
||||||
class UserProfileAdmin(admin.ModelAdmin):
|
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
list_display = ('avatar_tag','user','id','total_ratings','avg_rating','num_disputes','lost_disputes')
|
list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes')
|
||||||
list_display_links =['user']
|
list_display_links = ('avatar_tag','id')
|
||||||
|
change_links =['user']
|
||||||
readonly_fields = ['avatar_tag']
|
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.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
|
|
||||||
|
from decouple import config
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# TODO
|
# TODO
|
||||||
# Load hparams from .env file
|
# 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):
|
class Order(models.Model):
|
||||||
|
|
||||||
@ -25,30 +72,29 @@ class Order(models.Model):
|
|||||||
EUR = 2, 'EUR'
|
EUR = 2, 'EUR'
|
||||||
ETH = 3, 'ETH'
|
ETH = 3, 'ETH'
|
||||||
|
|
||||||
class Status(models.TextChoices):
|
class Status(models.IntegerChoices):
|
||||||
WFB = 0, 'Waiting for bond'
|
WFB = 0, 'Waiting for maker bond'
|
||||||
PUB = 1, 'Published in order book'
|
PUB = 1, 'Public'
|
||||||
DEL = 2, 'Deleted from order book'
|
DEL = 2, 'Deleted'
|
||||||
TAK = 3, 'Taken'
|
TAK = 3, 'Waiting for taker bond'
|
||||||
UCA = 4, 'Unilaterally cancelled'
|
UCA = 4, 'Cancelled'
|
||||||
RET = 5, 'Returned to order book' # Probably same as 1 in most cases.
|
WF2 = 5, 'Waiting for trade collateral and buyer invoice'
|
||||||
WF2 = 6, 'Waiting for trade collateral and buyer invoice'
|
WFE = 6, 'Waiting only for seller trade collateral'
|
||||||
WTC = 7, 'Waiting only for trade collateral'
|
WFI = 7, 'Waiting only for buyer invoice'
|
||||||
WBI = 8, 'Waiting only for buyer invoice'
|
CHA = 8, 'Sending fiat - In chatroom'
|
||||||
EXF = 9, 'Exchanging fiat / In chat'
|
CCA = 9, 'Collaboratively cancelled'
|
||||||
CCA = 10, 'Collaboratively cancelled'
|
FSE = 10, 'Fiat sent - In chatroom'
|
||||||
FSE = 11, 'Fiat sent'
|
FCO = 11, 'Fiat confirmed'
|
||||||
FCO = 12, 'Fiat confirmed'
|
SUC = 12, 'Sucessfully settled'
|
||||||
SUC = 13, 'Sucessfully settled'
|
FAI = 13, 'Failed lightning network routing'
|
||||||
FAI = 14, 'Failed lightning network routing'
|
UPI = 14, 'Updated invoice'
|
||||||
UPI = 15, 'Updated invoice'
|
DIS = 15, 'In dispute'
|
||||||
DIS = 16, 'In dispute'
|
MLD = 16, 'Maker lost dispute'
|
||||||
MLD = 17, 'Maker lost dispute'
|
TLD = 17, 'Taker lost dispute'
|
||||||
TLD = 18, 'Taker lost dispute'
|
EXP = 18, 'Expired'
|
||||||
EXP = 19, 'Expired'
|
|
||||||
|
|
||||||
# order info, id = models.CharField(max_length=64, unique=True, null=False)
|
# order info
|
||||||
status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB)
|
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
expires_at = models.DateTimeField()
|
expires_at = models.DateTimeField()
|
||||||
|
|
||||||
@ -56,42 +102,60 @@ class Order(models.Model):
|
|||||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
|
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
|
||||||
currency = models.PositiveSmallIntegerField(choices=Currencies.choices, null=False)
|
currency = models.PositiveSmallIntegerField(choices=Currencies.choices, null=False)
|
||||||
amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)])
|
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")
|
payment_method = models.CharField(max_length=30, null=False, default="not specified", blank=True)
|
||||||
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)])
|
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
||||||
is_explicit = models.BooleanField(default=False, null=False) # 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
|
# 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
|
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
|
# order collateral
|
||||||
has_maker_bond = models.BooleanField(default=False, null=False)
|
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
has_taker_bond = models.BooleanField(default=False, null=False)
|
taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
has_trade_collat = models.BooleanField(default=False, null=False)
|
trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# buyer payment LN invoice
|
# buyer payment LN invoice
|
||||||
has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid
|
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
invoice = models.CharField(max_length=300, unique=False, null=True, default=None)
|
|
||||||
|
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):
|
class Profile(models.Model):
|
||||||
|
|
||||||
user = models.OneToOneField(User,on_delete=models.CASCADE)
|
user = models.OneToOneField(User,on_delete=models.CASCADE)
|
||||||
|
|
||||||
# Ratings stored as a comma separated integer list
|
# Ratings stored as a comma separated integer list
|
||||||
total_ratings = models.PositiveIntegerField(null=False, default=0)
|
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
|
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)])
|
avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True)
|
||||||
|
|
||||||
# Disputes
|
# Disputes
|
||||||
num_disputes = models.PositiveIntegerField(null=False, default=0)
|
num_disputes = models.PositiveIntegerField(null=False, default=0)
|
||||||
lost_disputes = models.PositiveIntegerField(null=False, default=0)
|
lost_disputes = models.PositiveIntegerField(null=False, default=0)
|
||||||
|
|
||||||
# RoboHash
|
# 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)
|
@receiver(post_save, sender=User)
|
||||||
def create_user_profile(sender, instance, created, **kwargs):
|
def create_user_profile(sender, instance, created, **kwargs):
|
||||||
@ -102,15 +166,21 @@ class Profile(models.Model):
|
|||||||
def save_user_profile(sender, instance, **kwargs):
|
def save_user_profile(sender, instance, **kwargs):
|
||||||
instance.profile.save()
|
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):
|
def __str__(self):
|
||||||
return self.user.username
|
return self.user.username
|
||||||
|
|
||||||
# to display avatars in admin panel
|
# to display avatars in admin panel
|
||||||
def get_avatar(self):
|
def get_avatar(self):
|
||||||
if not self.avatar:
|
if not self.avatar:
|
||||||
return 'static/assets/avatars/unknown.png'
|
return 'static/assets/misc/unknown_avatar.png'
|
||||||
return self.avatar.url
|
return self.avatar.url
|
||||||
|
|
||||||
# method to create a fake table field in read only mode
|
# method to create a fake table field in read only mode
|
||||||
def avatar_tag(self):
|
def avatar_tag(self):
|
||||||
return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar())
|
return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar())
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Order
|
from .models import Order
|
||||||
|
|
||||||
class OrderSerializer(serializers.ModelSerializer):
|
class ListOrderSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker')
|
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:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis')
|
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 django.urls import path
|
||||||
from .views import MakeOrder, OrderView, UserGenerator, BookView
|
from .views import OrderMakerView, OrderView, UserView, BookView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('make/', MakeOrder.as_view()),
|
path('make/', OrderMakerView.as_view()),
|
||||||
path('order/', OrderView.as_view()),
|
path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})),
|
||||||
path('usergen/', UserGenerator.as_view()),
|
path('usergen/', UserView.as_view()),
|
||||||
path('book/', BookView.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.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.conf.urls.static import static
|
|
||||||
|
|
||||||
from .serializers import OrderSerializer, MakeOrderSerializer
|
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
|
||||||
from .models import Order
|
from .models import LNPayment, Order
|
||||||
|
from .logics import Logics
|
||||||
|
|
||||||
from .nick_generator.nick_generator import NickGenerator
|
from .nick_generator.nick_generator import NickGenerator
|
||||||
from robohash import Robohash
|
from robohash import Robohash
|
||||||
@ -17,94 +19,212 @@ import hashlib
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
# .env
|
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
|
||||||
expiration_time = 8
|
|
||||||
|
|
||||||
avatar_path = Path('frontend/static/assets/avatars')
|
avatar_path = Path('frontend/static/assets/avatars')
|
||||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
avatar_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
class MakeOrder(APIView):
|
class OrderMakerView(CreateAPIView):
|
||||||
serializer_class = MakeOrderSerializer
|
serializer_class = MakeOrderSerializer
|
||||||
|
|
||||||
def post(self,request):
|
def post(self,request):
|
||||||
serializer = self.serializer_class(data=request.data)
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
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')
|
|
||||||
|
|
||||||
# query if the user is already a maker or taker, return error
|
type = serializer.data.get('type')
|
||||||
queryset = Order.objects.filter(maker=request.user.id)
|
currency = serializer.data.get('currency')
|
||||||
if queryset.exists():
|
amount = serializer.data.get('amount')
|
||||||
return Response({'Bad Request':'You are already maker of an order'},status=status.HTTP_400_BAD_REQUEST)
|
payment_method = serializer.data.get('payment_method')
|
||||||
queryset = Order.objects.filter(taker=request.user.id)
|
premium = serializer.data.get('premium')
|
||||||
if queryset.exists():
|
satoshis = serializer.data.get('satoshis')
|
||||||
return Response({'Bad Request':'You are already taker of an order'},status=status.HTTP_400_BAD_REQUEST)
|
is_explicit = serializer.data.get('is_explicit')
|
||||||
|
|
||||||
# Creates a new order in db
|
valid, context = Logics.validate_already_maker_or_taker(request.user)
|
||||||
order = Order(
|
if not valid: return Response(context, status.HTTP_409_CONFLICT)
|
||||||
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()
|
|
||||||
|
|
||||||
if not serializer.is_valid():
|
# Creates a new order
|
||||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
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):
|
class OrderView(viewsets.ViewSet):
|
||||||
serializer_class = OrderSerializer
|
serializer_class = UpdateOrderSerializer
|
||||||
lookup_url_kwarg = 'order_id'
|
lookup_url_kwarg = 'order_id'
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
|
'''
|
||||||
|
Full trade pipeline takes place while looking/refreshing the order page.
|
||||||
|
'''
|
||||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
order_id = request.GET.get(self.lookup_url_kwarg)
|
||||||
|
|
||||||
if order_id != None:
|
if order_id == None:
|
||||||
order = Order.objects.filter(id=order_id)
|
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
|
order = Order.objects.filter(id=order_id)
|
||||||
if len(order) == 1 :
|
|
||||||
order = order[0]
|
|
||||||
data = self.serializer_class(order).data
|
|
||||||
nickname = request.user.username
|
|
||||||
|
|
||||||
# Check if requester is participant in the order and add boolean to response
|
# check if exactly one order is found in the db
|
||||||
data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname)
|
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
|
# This is our order.
|
||||||
data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value.
|
order = order[0]
|
||||||
|
|
||||||
data['maker_nick'] = str(order.maker)
|
# 1) If order expired
|
||||||
data['taker_nick'] = str(order.taker)
|
if order.status == Order.Status.EXP:
|
||||||
|
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
if data['is_participant']:
|
# 2) If order cancelled
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
if order.status == Order.Status.UCA:
|
||||||
else:
|
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
|
||||||
# Non participants should not see the status or who is the taker
|
if order.status == Order.Status.CCA:
|
||||||
data.pop('status','status_message','taker','taker_nick')
|
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
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'
|
lookup_url_kwarg = 'token'
|
||||||
NickGen = NickGenerator(
|
NickGen = NickGenerator(
|
||||||
lang='English',
|
lang='English',
|
||||||
@ -113,6 +233,7 @@ class UserGenerator(APIView):
|
|||||||
use_noun=True,
|
use_noun=True,
|
||||||
max_num=999)
|
max_num=999)
|
||||||
|
|
||||||
|
# Probably should be turned into a post method
|
||||||
def get(self,request, format=None):
|
def get(self,request, format=None):
|
||||||
'''
|
'''
|
||||||
Get a new user derived from a high entropy token
|
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)
|
value, counts = np.unique(list(token), return_counts=True)
|
||||||
shannon_entropy = entropy(counts, base=62)
|
shannon_entropy = entropy(counts, base=62)
|
||||||
bits_entropy = log2(len(value)**len(token))
|
bits_entropy = log2(len(value)**len(token))
|
||||||
|
# Payload
|
||||||
# Start preparing payload
|
|
||||||
context = {'token_shannon_entropy': shannon_entropy, 'token_bits_entropy': bits_entropy}
|
context = {'token_shannon_entropy': shannon_entropy, 'token_bits_entropy': bits_entropy}
|
||||||
|
|
||||||
# Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity
|
# 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.
|
# Hashes the token, only 1 iteration. Maybe more is better.
|
||||||
hash = hashlib.sha256(str.encode(token)).hexdigest()
|
hash = hashlib.sha256(str.encode(token)).hexdigest()
|
||||||
|
|
||||||
# generate nickname
|
# Generate nickname
|
||||||
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||||
context['nickname'] = nickname
|
context['nickname'] = nickname
|
||||||
|
|
||||||
# generate avatar
|
# Generate avatar
|
||||||
rh = Robohash(hash)
|
rh = Robohash(hash)
|
||||||
rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
|
rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
|
||||||
|
|
||||||
@ -155,7 +275,7 @@ class UserGenerator(APIView):
|
|||||||
with open(image_path, "wb") as f:
|
with open(image_path, "wb") as f:
|
||||||
rh.img.save(f, format="png")
|
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:
|
if len(User.objects.filter(username=nickname)) == 0:
|
||||||
User.objects.create_user(username=nickname, password=token, is_staff=False)
|
User.objects.create_user(username=nickname, password=token, is_staff=False)
|
||||||
user = authenticate(request, username=nickname, password=token)
|
user = authenticate(request, username=nickname, password=token)
|
||||||
@ -180,40 +300,40 @@ class UserGenerator(APIView):
|
|||||||
def delete(self,request):
|
def delete(self,request):
|
||||||
user = User.objects.get(id = request.user.id)
|
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
|
# However it might be a long time recovered user
|
||||||
# Only delete if user live is < 5 minutes
|
# Only delete if user live is < 5 minutes
|
||||||
|
|
||||||
# TODO check if user exists AND it is not a maker or taker!
|
# TODO check if user exists AND it is not a maker or taker!
|
||||||
if user is not None:
|
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)
|
logout(request)
|
||||||
user.delete()
|
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)
|
return Response(status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
class BookView(APIView):
|
class BookView(ListAPIView):
|
||||||
serializer_class = OrderSerializer
|
serializer_class = ListOrderSerializer
|
||||||
|
|
||||||
def get(self,request, format=None):
|
def get(self,request, format=None):
|
||||||
currency = request.GET.get('currency')
|
currency = request.GET.get('currency')
|
||||||
type = request.GET.get('type')
|
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:
|
if len(queryset)== 0:
|
||||||
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
|
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')
|
queryset = queryset.order_by('created_at')
|
||||||
book_data = []
|
book_data = []
|
||||||
for order in queryset:
|
for order in queryset:
|
||||||
data = OrderSerializer(order).data
|
data = ListOrderSerializer(order).data
|
||||||
user = User.objects.filter(id=data['maker'])
|
user = User.objects.filter(id=data['maker'])
|
||||||
if len(user) == 1:
|
if len(user) == 1:
|
||||||
data['maker_nick'] = user[0].username
|
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)
|
book_data.append(data)
|
||||||
|
|
||||||
return Response(book_data, status=status.HTTP_200_OK)
|
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 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 {
|
export default class BookPage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -13,18 +14,20 @@ export default class BookPage extends Component {
|
|||||||
this.state.currencyCode = this.getCurrencyCode(this.state.currency)
|
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
|
// Show message to be the first one to make an order
|
||||||
getOrderDetails() {
|
getOrderDetails() {
|
||||||
fetch('/api/book' + '?currency=' + this.state.currency + "&type=" + this.state.type)
|
fetch('/api/book' + '?currency=' + this.state.currency + "&type=" + this.state.type)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => //console.log(data));
|
.then((data) => //console.log(data));
|
||||||
this.setState({orders: data}));
|
this.setState({
|
||||||
|
orders: data,
|
||||||
|
not_found: data.not_found,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCardClick=(e)=>{
|
handleCardClick=(e)=>{
|
||||||
console.log(e.target)
|
console.log(e)
|
||||||
this.props.history.push('/order/' + e.target);
|
this.props.history.push('/order/' + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make these two functions sequential. getOrderDetails needs setState to be finish beforehand.
|
// 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, ",");
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Grid className='orderBook' container spacing={1}>
|
<Grid className='orderBook' container spacing={1}>
|
||||||
@ -102,66 +162,30 @@ export default class BookPage extends Component {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
{this.state.orders.map((order) =>
|
{ this.state.not_found ? "" :
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="h5" variant="h5">
|
<Typography component="h5" variant="h5">
|
||||||
You are {this.state.type == 0 ? " selling " : " buying "} BTC for {this.state.currencyCode}
|
You are {this.state.type == 0 ? " selling " : " buying "} BTC for {this.state.currencyCode}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</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">
|
<Grid item xs={12} align="center">
|
||||||
<Button color="secondary" variant="contained" to="/" component={Link}>
|
<Button color="secondary" variant="contained" to="/" component={Link}>
|
||||||
Back
|
Back
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core"
|
import { Paper, Alert, AlertTitle, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core"
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
@ -91,7 +91,7 @@ export default class MakerPage extends Component {
|
|||||||
console.log(this.state)
|
console.log(this.state)
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken},
|
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
type: this.state.type,
|
type: this.state.type,
|
||||||
currency: this.state.currency,
|
currency: this.state.currency,
|
||||||
@ -104,7 +104,8 @@ export default class MakerPage extends Component {
|
|||||||
};
|
};
|
||||||
fetch("/api/make/",requestOptions)
|
fetch("/api/make/",requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => (console.log(data) & this.props.history.push('/order/' + data.id)));
|
.then((data) => (this.setState({badRequest:data.bad_request})
|
||||||
|
& (data.id ? this.props.history.push('/order/' + data.id) :"")));
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -242,6 +243,13 @@ export default class MakerPage extends Component {
|
|||||||
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >
|
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >
|
||||||
Create Order
|
Create Order
|
||||||
</Button>
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} align="center">
|
||||||
|
{this.state.badRequest ?
|
||||||
|
<Typography component="subtitle2" variant="subtitle2" color="secondary">
|
||||||
|
{this.state.badRequest} <br/>
|
||||||
|
</Typography>
|
||||||
|
: ""}
|
||||||
<Typography component="subtitle2" variant="subtitle2">
|
<Typography component="subtitle2" variant="subtitle2">
|
||||||
<div align='center'>
|
<div align='center'>
|
||||||
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode}
|
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode}
|
||||||
|
@ -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 { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core"
|
||||||
import { Link } from 'react-router-dom'
|
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
|
// pretty numbers
|
||||||
function pn(x) {
|
function pn(x) {
|
||||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
@ -26,7 +54,7 @@ export default class OrderPage extends Component {
|
|||||||
statusText: data.status_message,
|
statusText: data.status_message,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
currency: data.currency,
|
currency: data.currency,
|
||||||
currencyCode: (data.currency== 1 ) ? "USD": ((data.currency == 2 ) ? "EUR":"ETH"),
|
currencyCode: this.getCurrencyCode(data.currency),
|
||||||
amount: data.amount,
|
amount: data.amount,
|
||||||
paymentMethod: data.payment_method,
|
paymentMethod: data.payment_method,
|
||||||
isExplicit: data.is_explicit,
|
isExplicit: data.is_explicit,
|
||||||
@ -37,14 +65,39 @@ export default class OrderPage extends Component {
|
|||||||
makerNick: data.maker_nick,
|
makerNick: data.maker_nick,
|
||||||
takerId: data.taker,
|
takerId: data.taker,
|
||||||
takerNick: data.taker_nick,
|
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
|
// Fix to use proper react props
|
||||||
handleClickBackButton=()=>{
|
handleClickBackButton=()=>{
|
||||||
window.history.back();
|
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 (){
|
render (){
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
@ -53,17 +106,43 @@ export default class OrderPage extends Component {
|
|||||||
BTC {this.state.type ? " Sell " : " Buy "} Order
|
BTC {this.state.type ? " Sell " : " Buy "} Order
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paper elevation={12} style={{ padding: 8,}}>
|
<Paper elevation={12} style={{ padding: 8,}}>
|
||||||
<List component="nav" aria-label="mailbox folders">
|
<List dense="true">
|
||||||
<ListItem>
|
<ListItem >
|
||||||
<ListItemAvatar sx={{ width: 56, height: 56 }}>
|
<ListItemAvatar sx={{ width: 56, height: 56 }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
alt={this.state.makerNick}
|
alt={this.state.makerNick}
|
||||||
src={window.location.origin +'/static/assets/avatars/' + this.state.makerNick + '.png'}
|
src={window.location.origin +'/static/assets/avatars/' + this.state.makerNick + '.png'}
|
||||||
/>
|
/>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={this.state.makerNick} secondary="Order maker" />
|
<ListItemText primary={this.state.makerNick} secondary="Order maker" align="right"/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider />
|
<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>
|
<ListItem>
|
||||||
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))+" "+this.state.currencyCode} secondary="Amount and currency requested"/>
|
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))+" "+this.state.currencyCode} secondary="Amount and currency requested"/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -80,32 +159,26 @@ export default class OrderPage extends Component {
|
|||||||
}
|
}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider />
|
<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>
|
<ListItem>
|
||||||
<ListItemText primary={'#'+this.orderId} secondary="Order ID"/>
|
<ListItemText primary={'#'+this.orderId} secondary="Order ID"/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary={msToTime( new Date(this.state.expiresAt) - Date.now())} secondary="Expires in "/>
|
||||||
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
</Paper>
|
||||||
|
|
||||||
<Grid item xs={12} align="center">
|
<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>
|
||||||
<Grid item xs={12} align="center">
|
<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>
|
</Grid>
|
||||||
</Paper>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</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() {
|
delGeneratedUser() {
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken},
|
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
||||||
};
|
};
|
||||||
fetch("/api/usergen", requestOptions)
|
fetch("/api/usergen", requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
@ -74,7 +74,7 @@ export default class UserGenPage extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
token: this.genBase62Token(32),
|
token: this.genBase62Token(32),
|
||||||
})
|
})
|
||||||
this.getGeneratedUser();
|
this.reload_for_csrf_to_work();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeToken=(e)=>{
|
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`
|
`sudo apt install python3 python3 pip`
|
||||||
|
|
||||||
### Install virtual environments
|
### Install virtual environments
|
||||||
`pip install virtualenvwrapper`
|
```
|
||||||
|
pip install virtualenvwrapper
|
||||||
|
pip install python-decouple
|
||||||
|
```
|
||||||
|
|
||||||
### Add to .bashrc
|
### Add to .bashrc
|
||||||
|
|
||||||
@ -28,6 +31,9 @@ source /usr/local/bin/virtualenvwrapper.sh
|
|||||||
### Install Django and Restframework
|
### Install Django and Restframework
|
||||||
`pip3 install django djangorestframework`
|
`pip3 install django djangorestframework`
|
||||||
|
|
||||||
|
## Install Django admin relational links
|
||||||
|
`pip install django-admin-relation-links`
|
||||||
|
|
||||||
*Django 4.0 at the time of writting*
|
*Django 4.0 at the time of writting*
|
||||||
|
|
||||||
### Launch the local development node
|
### Launch the local development node
|
||||||
|
Loading…
Reference in New Issue
Block a user