mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-19 04:31:35 +00:00
merge htlc-model
This commit is contained in:
parent
4516f1974a
commit
215af668a2
@ -24,13 +24,13 @@ class EUserAdmin(UserAdmin):
|
|||||||
|
|
||||||
@admin.register(Order)
|
@admin.register(Order)
|
||||||
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
list_display = ('id','type','maker_link','taker_link','status','amount','currency','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
|
list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
|
||||||
list_display_links = ('id','type')
|
list_display_links = ('id','type')
|
||||||
change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow')
|
change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow')
|
||||||
|
|
||||||
@admin.register(LNPayment)
|
@admin.register(LNPayment)
|
||||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
list_display = ('id','concept','status','amount','type','invoice','secret','expires_at','sender_link','receiver_link')
|
list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link')
|
||||||
list_display_links = ('id','concept')
|
list_display_links = ('id','concept')
|
||||||
change_links = ('sender','receiver')
|
change_links = ('sender','receiver')
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
@ -10,17 +13,28 @@ class LNNode():
|
|||||||
Place holder functions to interact with Lightning Node
|
Place holder functions to interact with Lightning Node
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def gen_hodl_invoice():
|
def gen_hodl_invoice(num_satoshis, description, expiry):
|
||||||
'''Generates hodl invoice to publish an order'''
|
'''Generates hodl invoice to publish an order'''
|
||||||
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=80))
|
# TODO
|
||||||
|
invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX
|
||||||
|
payment_hash = ''.join(random.choices(string.ascii_uppercase + string.digits, k=40)) #FIX
|
||||||
|
expires_at = timezone.now() + timedelta(hours=8) ##FIX
|
||||||
|
|
||||||
|
return invoice, payment_hash, expires_at
|
||||||
|
|
||||||
def validate_hodl_invoice_locked():
|
def validate_hodl_invoice_locked():
|
||||||
'''Generates hodl invoice to publish an order'''
|
'''Generates hodl invoice to publish an order'''
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def validate_ln_invoice(invoice):
|
def validate_ln_invoice(invoice): # num_satoshis
|
||||||
'''Checks if a LN invoice is valid'''
|
'''Checks if the submited LN invoice is as expected'''
|
||||||
return True
|
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):
|
def pay_buyer_invoice(invoice):
|
||||||
'''Sends sats to buyer'''
|
'''Sends sats to buyer'''
|
||||||
|
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.currency_dict[str(order.currency)]]['last'])
|
||||||
|
satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000
|
||||||
|
|
||||||
|
return int(satoshis_now)
|
||||||
|
|
||||||
|
def order_expires(order):
|
||||||
|
order.status = Order.Status.EXP
|
||||||
|
order.maker = None
|
||||||
|
order.taker = None
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def buyer_invoice_amount(cls, order, user):
|
||||||
|
''' Computes buyer invoice amount. Uses order.last_satoshis,
|
||||||
|
that is the final trade amount set at Taker Bond time'''
|
||||||
|
|
||||||
|
if cls.is_buyer(order, user):
|
||||||
|
invoice_amount = int(order.last_satoshis * (1-FEE)) # Trading FEE is charged here.
|
||||||
|
|
||||||
|
return True, {'invoice_amount': invoice_amount}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_invoice(cls, order, user, invoice):
|
||||||
|
is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice)
|
||||||
|
# only user is the buyer and a valid LN invoice
|
||||||
|
if not (cls.is_buyer(order, user) or is_valid_invoice):
|
||||||
|
return False, {'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}
|
||||||
|
|
||||||
|
order.buyer_invoice, _ = LNPayment.objects.update_or_create(
|
||||||
|
concept = LNPayment.Concepts.PAYBUYER,
|
||||||
|
type = LNPayment.Types.NORM,
|
||||||
|
sender = User.objects.get(username=ESCROW_USERNAME),
|
||||||
|
receiver= user,
|
||||||
|
# if there is a LNPayment matching these above, it updates that one with defaults below.
|
||||||
|
defaults={
|
||||||
|
'invoice' : invoice,
|
||||||
|
'status' : LNPayment.Status.VALIDI,
|
||||||
|
'num_satoshis' : num_satoshis,
|
||||||
|
'description' : description,
|
||||||
|
'payment_hash' : payment_hash,
|
||||||
|
'expires_at' : expires_at}
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the order status is 'Waiting for invoice'. Move forward to 'waiting for invoice'
|
||||||
|
if order.status == Order.Status.WFE: order.status = Order.Status.CHA
|
||||||
|
|
||||||
|
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' or to 'chat'
|
||||||
|
if order.status == Order.Status.WF2:
|
||||||
|
print(order.trade_escrow)
|
||||||
|
if order.trade_escrow:
|
||||||
|
if order.trade_escrow.status == LNPayment.Status.LOCKED:
|
||||||
|
order.status = Order.Status.CHA
|
||||||
|
else:
|
||||||
|
order.status = Order.Status.WFE
|
||||||
|
|
||||||
|
# If the order status was Payment Failed. Move forward to invoice Updated.
|
||||||
|
if order.status == Order.Status.FAI:
|
||||||
|
order.status = Order.Status.UPI
|
||||||
|
|
||||||
|
order.save()
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def rate_counterparty(cls, order, user, rating):
|
||||||
|
# if maker, rates taker
|
||||||
|
if order.maker == user:
|
||||||
|
order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1
|
||||||
|
last_ratings = list(order.taker.profile.last_ratings).append(rating)
|
||||||
|
order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
|
||||||
|
# if taker, rates maker
|
||||||
|
if order.taker == user:
|
||||||
|
order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1
|
||||||
|
last_ratings = list(order.maker.profile.last_ratings).append(rating)
|
||||||
|
order.maker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
|
||||||
|
|
||||||
|
order.save()
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def cancel_order(cls, order, user, state):
|
||||||
|
|
||||||
|
# 1) When maker cancels before bond
|
||||||
|
'''The order never shows up on the book and order
|
||||||
|
status becomes "cancelled". That's it.'''
|
||||||
|
if order.status == Order.Status.WFB and order.maker == user:
|
||||||
|
order.maker = None
|
||||||
|
order.status = Order.Status.UCA
|
||||||
|
order.save()
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
# 2) When maker cancels after bond
|
||||||
|
'''The order dissapears from book and goes to cancelled.
|
||||||
|
Maker is charged a small amount of sats, to prevent DDOS
|
||||||
|
on the LN node and order book'''
|
||||||
|
|
||||||
|
# 3) When taker cancels before bond
|
||||||
|
''' The order goes back to the book as public.
|
||||||
|
LNPayment "order.taker_bond" is deleted() '''
|
||||||
|
|
||||||
|
# 4) When taker or maker cancel after bond (before escrow)
|
||||||
|
'''The order goes into cancelled status if maker cancels.
|
||||||
|
The order goes into the public book if taker cancels.
|
||||||
|
In both cases there is a small fee.'''
|
||||||
|
|
||||||
|
# 5) When trade collateral has been posted (after escrow)
|
||||||
|
'''Always goes to cancelled status. Collaboration is needed.
|
||||||
|
When a user asks for cancel, 'order.is_pending_cancel' goes True.
|
||||||
|
When the second user asks for cancel. Order is totally cancelled.
|
||||||
|
Has a small cost for both parties to prevent node DDOS.'''
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False, {'bad_request':'You cannot cancel this order'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_maker_hodl_invoice(cls, order, user):
|
||||||
|
|
||||||
|
# Do not gen and cancel if order is more than 5 minutes old
|
||||||
|
if order.expires_at < timezone.now():
|
||||||
|
cls.order_expires(order)
|
||||||
|
return False, {'bad_request':'Invoice expired. You did not confirm publishing the order in time. Make a new order.'}
|
||||||
|
|
||||||
|
# Return the previous invoice if there was one and is still unpaid
|
||||||
|
if order.maker_bond:
|
||||||
|
if order.maker_bond.status == LNPayment.Status.INVGEN:
|
||||||
|
return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis}
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
order.last_satoshis = cls.satoshis_now(order)
|
||||||
|
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||||
|
description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.'
|
||||||
|
|
||||||
|
# Gen HODL Invoice
|
||||||
|
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
|
||||||
|
|
||||||
|
order.maker_bond = LNPayment.objects.create(
|
||||||
|
concept = LNPayment.Concepts.MAKEBOND,
|
||||||
|
type = LNPayment.Types.HODL,
|
||||||
|
sender = user,
|
||||||
|
receiver = User.objects.get(username=ESCROW_USERNAME),
|
||||||
|
invoice = invoice,
|
||||||
|
status = LNPayment.Status.INVGEN,
|
||||||
|
num_satoshis = bond_satoshis,
|
||||||
|
description = description,
|
||||||
|
payment_hash = payment_hash,
|
||||||
|
expires_at = expires_at)
|
||||||
|
|
||||||
|
order.save()
|
||||||
|
return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_taker_hodl_invoice(cls, order, user):
|
||||||
|
|
||||||
|
# Do not gen and cancel if a taker invoice is there and older than X minutes and unpaid still
|
||||||
|
if order.taker_bond:
|
||||||
|
# Check if status is INVGEN and still not expired
|
||||||
|
if order.taker_bond.status == LNPayment.Status.INVGEN:
|
||||||
|
if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)):
|
||||||
|
cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond
|
||||||
|
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
|
||||||
|
# Return the previous invoice there was with INVGEN status
|
||||||
|
else:
|
||||||
|
return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
|
||||||
|
# Invoice exists, but was already locked or settled
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE
|
||||||
|
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||||
|
description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat.'
|
||||||
|
|
||||||
|
# Gen HODL Invoice
|
||||||
|
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
|
||||||
|
|
||||||
|
order.taker_bond = LNPayment.objects.create(
|
||||||
|
concept = LNPayment.Concepts.TAKEBOND,
|
||||||
|
type = LNPayment.Types.HODL,
|
||||||
|
sender = user,
|
||||||
|
receiver = User.objects.get(username=ESCROW_USERNAME),
|
||||||
|
invoice = invoice,
|
||||||
|
status = LNPayment.Status.INVGEN,
|
||||||
|
num_satoshis = bond_satoshis,
|
||||||
|
description = description,
|
||||||
|
payment_hash = payment_hash,
|
||||||
|
expires_at = expires_at)
|
||||||
|
|
||||||
|
order.save()
|
||||||
|
return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_escrow_hodl_invoice(cls, order, user):
|
||||||
|
|
||||||
|
# Do not generate and cancel if an invoice is there and older than X minutes and unpaid still
|
||||||
|
if order.trade_escrow:
|
||||||
|
# Check if status is INVGEN and still not expired
|
||||||
|
if order.taker_bond.status == LNPayment.Status.INVGEN:
|
||||||
|
if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TRADE_ESCR_INVOICE)): # Expired
|
||||||
|
cls.cancel_order(order, user, 4) # State 4, cancel order before trade escrow locked
|
||||||
|
return False, {'bad_request':'Invoice expired. You did not lock the trade escrow in time.'}
|
||||||
|
# Return the previous invoice there was with INVGEN status
|
||||||
|
else:
|
||||||
|
return True, {'escrow_invoice':order.trade_escrow.invoice,'escrow_satoshis':order.trade_escrow.num_satoshis}
|
||||||
|
# Invoice exists, but was already locked or settled
|
||||||
|
else:
|
||||||
|
return False, None # Does not return any context of a healthy locked escrow
|
||||||
|
|
||||||
|
escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis)
|
||||||
|
description = f'RoboSats - Escrow amount for {str(order)} - This escrow will be released to the buyer once you confirm you received the fiat.'
|
||||||
|
|
||||||
|
# Gen HODL Invoice
|
||||||
|
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
|
||||||
|
|
||||||
|
order.taker_bond = LNPayment.objects.create(
|
||||||
|
concept = LNPayment.Concepts.TRESCROW,
|
||||||
|
type = LNPayment.Types.HODL,
|
||||||
|
sender = user,
|
||||||
|
receiver = User.objects.get(username=ESCROW_USERNAME),
|
||||||
|
invoice = invoice,
|
||||||
|
status = LNPayment.Status.INVGEN,
|
||||||
|
num_satoshis = escrow_satoshis,
|
||||||
|
description = description,
|
||||||
|
payment_hash = payment_hash,
|
||||||
|
expires_at = expires_at)
|
||||||
|
|
||||||
|
order.save()
|
||||||
|
return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis}
|
104
api/models.py
104
api/models.py
@ -3,9 +3,9 @@ from django.contrib.auth.models import User
|
|||||||
from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
|
from django.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
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -13,10 +13,11 @@ import json
|
|||||||
# TODO
|
# TODO
|
||||||
# Load hparams from .env file
|
# Load hparams from .env file
|
||||||
|
|
||||||
MIN_TRADE = 10*1000 #In sats
|
MIN_TRADE = int(config('MIN_TRADE'))
|
||||||
MAX_TRADE = 500*1000
|
MAX_TRADE = int(config('MAX_TRADE'))
|
||||||
FEE = 0.002 # Trade fee in %
|
FEE = float(config('FEE'))
|
||||||
BOND_SIZE = 0.01 # Bond in %
|
BOND_SIZE = float(config('BOND_SIZE'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LNPayment(models.Model):
|
class LNPayment(models.Model):
|
||||||
@ -27,38 +28,39 @@ class LNPayment(models.Model):
|
|||||||
|
|
||||||
class Concepts(models.IntegerChoices):
|
class Concepts(models.IntegerChoices):
|
||||||
MAKEBOND = 0, 'Maker bond'
|
MAKEBOND = 0, 'Maker bond'
|
||||||
TAKEBOND = 1, 'Taker-buyer bond'
|
TAKEBOND = 1, 'Taker bond'
|
||||||
TRESCROW = 2, 'Trade escrow'
|
TRESCROW = 2, 'Trade escrow'
|
||||||
PAYBUYER = 3, 'Payment to buyer'
|
PAYBUYER = 3, 'Payment to buyer'
|
||||||
|
|
||||||
class Status(models.IntegerChoices):
|
class Status(models.IntegerChoices):
|
||||||
INVGEN = 0, 'Hodl invoice was generated'
|
INVGEN = 0, 'Generated'
|
||||||
LOCKED = 1, 'Hodl invoice has HTLCs locked'
|
LOCKED = 1, 'Locked'
|
||||||
CHRGED = 2, 'Hodl invoice was charged'
|
SETLED = 2, 'Settled'
|
||||||
RETNED = 3, 'Hodl invoice was returned'
|
RETNED = 3, 'Returned'
|
||||||
MISSNG = 4, 'Buyer invoice is missing'
|
MISSNG = 4, 'Missing'
|
||||||
IVALID = 5, 'Buyer invoice is valid'
|
VALIDI = 5, 'Valid'
|
||||||
INPAID = 6, 'Buyer invoice was paid'
|
INFAIL = 6, 'Failed routing'
|
||||||
INFAIL = 7, 'Buyer invoice routing failed'
|
|
||||||
|
|
||||||
# payment use case
|
# payment use details
|
||||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL)
|
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL)
|
||||||
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
||||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
|
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
|
||||||
|
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
|
||||||
|
|
||||||
# payment details
|
# payment info
|
||||||
invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
|
invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
|
||||||
secret = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
|
payment_hash = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
|
||||||
|
description = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
expires_at = models.DateTimeField()
|
expires_at = models.DateTimeField()
|
||||||
amount = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
|
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
|
||||||
|
|
||||||
# payment relationals
|
# involved parties
|
||||||
sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None)
|
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)
|
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# Make relational back to ORDER
|
return (f'HTLC {self.id}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
|
||||||
return (f'HTLC {self.id}: {self.Concepts(self.concept).label}')
|
|
||||||
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
|
|
||||||
@ -67,26 +69,25 @@ class Order(models.Model):
|
|||||||
SELL = 1, 'SELL'
|
SELL = 1, 'SELL'
|
||||||
|
|
||||||
class Status(models.IntegerChoices):
|
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'
|
|
||||||
|
|
||||||
currency_dict = json.load(open('./api/currencies.json'))
|
currency_dict = json.load(open('./api/currencies.json'))
|
||||||
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
|
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
|
||||||
@ -104,15 +105,18 @@ class Order(models.Model):
|
|||||||
|
|
||||||
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
||||||
is_explicit = models.BooleanField(default=False, null=False)
|
is_explicit = models.BooleanField(default=False, null=False)
|
||||||
# marked to marked
|
# marked to market
|
||||||
premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
|
premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
|
||||||
t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True)
|
|
||||||
# explicit
|
# explicit
|
||||||
satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True)
|
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, blank=True) # 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
|
||||||
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
@ -122,6 +126,19 @@ class Order(models.Model):
|
|||||||
# buyer payment LN invoice
|
# buyer payment LN invoice
|
||||||
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
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):
|
class Profile(models.Model):
|
||||||
|
|
||||||
@ -165,3 +182,4 @@ class Profile(models.Model):
|
|||||||
# 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())
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class MakeOrderSerializer(serializers.ModelSerializer):
|
|||||||
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.ModelSerializer):
|
class UpdateOrderSerializer(serializers.Serializer):
|
||||||
class Meta:
|
invoice = serializers.CharField(max_length=300, allow_null=True, allow_blank=True, default=None)
|
||||||
model = Order
|
action = serializers.ChoiceField(choices=('take','update_invoice','dispute','cancel','confirm','rate'), allow_null=False)
|
||||||
fields = ('id','buyer_invoice')
|
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)
|
@ -3,7 +3,7 @@ from .views import OrderMakerView, OrderView, UserView, BookView, get_currencies
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('make/', OrderMakerView.as_view()),
|
path('make/', OrderMakerView.as_view()),
|
||||||
path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})),
|
path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})),
|
||||||
path('usergen/', UserView.as_view()),
|
path('usergen/', UserView.as_view()),
|
||||||
path('book/', BookView.as_view()),
|
path('book/', BookView.as_view()),
|
||||||
path('currencies/', get_currencies_json),
|
path('currencies/', get_currencies_json),
|
||||||
|
212
api/views.py
212
api/views.py
@ -1,15 +1,14 @@
|
|||||||
from rest_framework import status, serializers
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.generics import CreateAPIView, ListAPIView
|
from rest_framework.generics import CreateAPIView, ListAPIView
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework import viewsets
|
|
||||||
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 .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
|
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
|
||||||
from .models import Order, LNPayment
|
from .models import LNPayment, Order
|
||||||
from .lightning import LNNode
|
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
|
||||||
@ -20,29 +19,16 @@ 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
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
# .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)
|
||||||
|
|
||||||
def validate_already_maker_or_taker(request):
|
|
||||||
'''Checks if the user is already partipant of an order'''
|
|
||||||
|
|
||||||
queryset = Order.objects.filter(maker=request.user.id)
|
|
||||||
if queryset.exists():
|
|
||||||
return False, Response({'Bad Request':'You are already maker of an order'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
queryset = Order.objects.filter(taker=request.user.id)
|
|
||||||
if queryset.exists():
|
|
||||||
return False, Response({'Bad Request':'You are already taker of an order'}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
return True, None
|
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
class OrderMakerView(CreateAPIView):
|
class OrderMakerView(CreateAPIView):
|
||||||
@ -51,8 +37,9 @@ class OrderMakerView(CreateAPIView):
|
|||||||
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')
|
|
||||||
|
type = serializer.data.get('type')
|
||||||
currency = serializer.data.get('currency')
|
currency = serializer.data.get('currency')
|
||||||
amount = serializer.data.get('amount')
|
amount = serializer.data.get('amount')
|
||||||
payment_method = serializer.data.get('payment_method')
|
payment_method = serializer.data.get('payment_method')
|
||||||
@ -60,27 +47,28 @@ class OrderMakerView(CreateAPIView):
|
|||||||
satoshis = serializer.data.get('satoshis')
|
satoshis = serializer.data.get('satoshis')
|
||||||
is_explicit = serializer.data.get('is_explicit')
|
is_explicit = serializer.data.get('is_explicit')
|
||||||
|
|
||||||
valid, response = validate_already_maker_or_taker(request)
|
valid, context = Logics.validate_already_maker_or_taker(request.user)
|
||||||
if not valid:
|
if not valid: return Response(context, status.HTTP_409_CONFLICT)
|
||||||
return response
|
|
||||||
|
|
||||||
# Creates a new order in db
|
# Creates a new order
|
||||||
order = Order(
|
order = Order(
|
||||||
type=otype,
|
type=type,
|
||||||
status=Order.Status.PUB, # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond)
|
|
||||||
currency=currency,
|
currency=currency,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
payment_method=payment_method,
|
payment_method=payment_method,
|
||||||
premium=premium,
|
premium=premium,
|
||||||
satoshis=satoshis,
|
satoshis=satoshis,
|
||||||
is_explicit=is_explicit,
|
is_explicit=is_explicit,
|
||||||
expires_at= timezone.now()+timedelta(hours=expiration_time),
|
expires_at=timezone.now()+timedelta(minutes=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
|
||||||
maker=request.user)
|
maker=request.user)
|
||||||
|
|
||||||
|
# TODO move to Order class method when new instance is created!
|
||||||
|
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
|
||||||
|
|
||||||
|
valid, context = Logics.validate_order_size(order)
|
||||||
|
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
if not serializer.is_valid():
|
|
||||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
|
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
@ -89,87 +77,154 @@ class OrderView(viewsets.ViewSet):
|
|||||||
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:
|
||||||
|
return Response({'bad_request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
order = Order.objects.filter(id=order_id)
|
order = Order.objects.filter(id=order_id)
|
||||||
|
|
||||||
# check if exactly one order is found in the db
|
# check if exactly one order is found in the db
|
||||||
if len(order) == 1 :
|
if len(order) != 1 :
|
||||||
|
return Response({'bad_request':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# This is our order.
|
||||||
order = order[0]
|
order = order[0]
|
||||||
|
|
||||||
|
# 1) If order expired
|
||||||
|
if order.status == Order.Status.EXP:
|
||||||
|
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 2) If order cancelled
|
||||||
|
if order.status == Order.Status.UCA:
|
||||||
|
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
|
||||||
|
if order.status == Order.Status.CCA:
|
||||||
|
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
data = ListOrderSerializer(order).data
|
data = ListOrderSerializer(order).data
|
||||||
nickname = request.user.username
|
|
||||||
|
|
||||||
# Add booleans if user is maker, taker, partipant, buyer or seller
|
# Add booleans if user is maker, taker, partipant, buyer or seller
|
||||||
data['is_maker'] = str(order.maker) == nickname
|
data['is_maker'] = order.maker == request.user
|
||||||
data['is_taker'] = str(order.taker) == nickname
|
data['is_taker'] = order.taker == request.user
|
||||||
data['is_participant'] = data['is_maker'] or data['is_taker']
|
data['is_participant'] = data['is_maker'] or data['is_taker']
|
||||||
data['is_buyer'] = (data['is_maker'] and order.type == Order.Types.BUY) or (data['is_taker'] and order.type == Order.Types.SELL)
|
|
||||||
data['is_seller'] = (data['is_maker'] and order.type == Order.Types.SELL) or (data['is_taker'] and order.type == Order.Types.BUY)
|
|
||||||
|
|
||||||
# If not a participant and order is not public, forbid.
|
# 3) If not a participant and order is not public, forbid.
|
||||||
if not data['is_participant'] and order.status != Order.Status.PUB:
|
if not data['is_participant'] and order.status != Order.Status.PUB:
|
||||||
return Response({'bad_request':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN)
|
return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
# return nicks too
|
# 4) Non participants can view details (but only if PUB)
|
||||||
|
elif not data['is_participant'] and order.status != Order.Status.PUB:
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# For participants add position side, nicks and status as message
|
||||||
|
data['is_buyer'] = Logics.is_buyer(order,request.user)
|
||||||
|
data['is_seller'] = Logics.is_seller(order,request.user)
|
||||||
data['maker_nick'] = str(order.maker)
|
data['maker_nick'] = str(order.maker)
|
||||||
data['taker_nick'] = str(order.taker)
|
data['taker_nick'] = str(order.taker)
|
||||||
|
|
||||||
data['status_message'] = Order.Status(order.status).label
|
data['status_message'] = Order.Status(order.status).label
|
||||||
|
|
||||||
if data['is_participant']:
|
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice.
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
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:
|
else:
|
||||||
# Non participants should not see the status, who is the taker, etc
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
for key in ('status','status_message','taker','taker_nick','is_maker','is_taker','is_buyer','is_seller'):
|
|
||||||
del data[key]
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND)
|
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER HODL invoice.
|
||||||
return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
|
elif order.status == Order.Status.TAK and data['is_taker']:
|
||||||
|
valid, context = Logics.gen_taker_hodl_invoice(order, request.user)
|
||||||
|
if valid:
|
||||||
|
data = {**data, **context}
|
||||||
|
else:
|
||||||
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 7) If status is 'WF2'or'WTC'
|
||||||
|
elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE):
|
||||||
|
|
||||||
def take_or_update(self, request, format=None):
|
# If the two bonds are locked
|
||||||
|
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||||
|
|
||||||
|
# 7.a) And if user is Seller, reply with an ESCROW HODL invoice.
|
||||||
|
if data['is_seller']:
|
||||||
|
valid, context = Logics.gen_escrow_hodl_invoice(order, request.user)
|
||||||
|
if valid:
|
||||||
|
data = {**data, **context}
|
||||||
|
else:
|
||||||
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 7.b) If user is Buyer, reply with an AMOUNT so he can send the buyer invoice.
|
||||||
|
elif data['is_buyer']:
|
||||||
|
valid, context = Logics.buyer_invoice_amount(order, request.user)
|
||||||
|
if valid:
|
||||||
|
data = {**data, **context}
|
||||||
|
else:
|
||||||
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 8) If status is 'CHA'or '' or '' and all HTLCS are in LOCKED
|
||||||
|
elif order.status == Order.Status.CHA: # TODO Add the other status
|
||||||
|
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
|
||||||
|
# add whether a collaborative cancel is pending
|
||||||
|
data['pending_cancel'] = order.is_pending_cancel
|
||||||
|
|
||||||
|
return Response(data, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def take_update_confirm_dispute_cancel(self, request, format=None):
|
||||||
|
'''
|
||||||
|
Here take place all of the user updates to the order object.
|
||||||
|
That is: take, confim, cancel, dispute, update_invoice or rate.
|
||||||
|
'''
|
||||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
order_id = request.GET.get(self.lookup_url_kwarg)
|
||||||
|
|
||||||
serializer = UpdateOrderSerializer(data=request.data)
|
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)
|
order = Order.objects.get(id=order_id)
|
||||||
|
|
||||||
if serializer.is_valid():
|
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' 6)'rate' (counterparty)
|
||||||
invoice = serializer.data.get('buyer_invoice')
|
action = serializer.data.get('action')
|
||||||
|
invoice = serializer.data.get('invoice')
|
||||||
|
rating = serializer.data.get('rating')
|
||||||
|
|
||||||
# If this is an empty POST request (no invoice), it must be taker request!
|
# 1) If action is take, it is be taker request!
|
||||||
if not invoice and order.status == Order.Status.PUB:
|
if action == 'take':
|
||||||
|
if order.status == Order.Status.PUB:
|
||||||
|
valid, context = Logics.validate_already_maker_or_taker(request.user)
|
||||||
|
if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
valid, response = validate_already_maker_or_taker(request)
|
Logics.take(order, request.user)
|
||||||
if not valid:
|
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
|
||||||
return response
|
|
||||||
|
|
||||||
order.taker = self.request.user
|
# 2) If action is update (invoice)
|
||||||
order.status = Order.Status.TAK
|
elif action == 'update_invoice' and invoice:
|
||||||
|
valid, context = Logics.update_invoice(order,request.user,invoice)
|
||||||
|
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
#TODO REPLY WITH HODL INVOICE
|
# 3) If action is cancel
|
||||||
data = ListOrderSerializer(order).data
|
elif action == 'cancel':
|
||||||
|
valid, context = Logics.cancel_order(order,request.user)
|
||||||
|
if not valid: return Response(context,status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# An invoice came in! update it
|
# 4) If action is confirm
|
||||||
elif invoice:
|
elif action == 'confirm':
|
||||||
if LNNode.validate_ln_invoice(invoice):
|
pass
|
||||||
order.invoice = invoice
|
|
||||||
|
|
||||||
#TODO Validate if request comes from PARTICIPANT AND BUYER
|
# 5) If action is dispute
|
||||||
|
elif action == 'dispute':
|
||||||
|
pass
|
||||||
|
|
||||||
#If the order status was Payment Failed. Move foward to invoice Updated.
|
# 6) If action is dispute
|
||||||
if order.status == Order.Status.FAI:
|
elif action == 'rate' and rating:
|
||||||
order.status = Order.Status.UPI
|
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:
|
else:
|
||||||
return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'})
|
return Response({'bad_request':'The Robotic Satoshis working in the warehouse did not understand you'})
|
||||||
|
|
||||||
# Something else is going on. Probably not allowed.
|
|
||||||
else:
|
|
||||||
return Response({'bad_request':'Not allowed'})
|
|
||||||
|
|
||||||
order.save()
|
|
||||||
return self.get(request)
|
return self.get(request)
|
||||||
|
|
||||||
class UserView(APIView):
|
class UserView(APIView):
|
||||||
@ -223,7 +278,7 @@ class UserView(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)
|
||||||
@ -279,6 +334,7 @@ class BookView(ListAPIView):
|
|||||||
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
|
||||||
|
|
||||||
# Non participants should not see the status or who is the taker
|
# Non participants should not see the status or who is the taker
|
||||||
for key in ('status','taker'):
|
for key in ('status','taker'):
|
||||||
del data[key]
|
del data[key]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, Link, RouterLink, ListItemAvatar} from "@material-ui/core"
|
import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@material-ui/core"
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
export default class BookPage extends Component {
|
export default class BookPage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -15,7 +16,6 @@ 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)
|
||||||
@ -26,15 +26,6 @@ export default class BookPage extends Component {
|
|||||||
not_found: data.not_found,
|
not_found: data.not_found,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
getCurrencyDict() {
|
|
||||||
fetch('/api/currencies')
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) =>
|
|
||||||
this.setState({
|
|
||||||
currencies_dict: data
|
|
||||||
}));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCardClick=(e)=>{
|
handleCardClick=(e)=>{
|
||||||
console.log(e)
|
console.log(e)
|
||||||
@ -56,6 +47,14 @@ export default class BookPage extends Component {
|
|||||||
this.getOrderDetails();
|
this.getOrderDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCurrencyDict() {
|
||||||
|
fetch('/api/currencies')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) =>
|
||||||
|
this.setState({
|
||||||
|
currencies_dict: data
|
||||||
|
}));
|
||||||
|
}
|
||||||
// Gets currency code (3 letters) from numeric (e.g., 1 -> USD)
|
// Gets currency code (3 letters) from numeric (e.g., 1 -> USD)
|
||||||
// Improve this function so currencies are read from json
|
// Improve this function so currencies are read from json
|
||||||
getCurrencyCode(val){
|
getCurrencyCode(val){
|
||||||
@ -101,14 +100,14 @@ export default class BookPage extends Component {
|
|||||||
<Typography variant="subtitle1" color="text.secondary">
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
◑ Payment via <b>{order.payment_method}</b>
|
◑ Payment via <b>{order.payment_method}</b>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{/*
|
||||||
<Typography variant="subtitle1" color="text.secondary">
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
◑ Priced {order.is_explicit ?
|
◑ Priced {order.is_explicit ?
|
||||||
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
|
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
|
||||||
" at " +
|
" at " +
|
||||||
parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market"
|
parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market"
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography> */}
|
||||||
|
|
||||||
<Typography variant="subtitle1" color="text.secondary">
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
◑ <b>{" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC</b> (Binance API)
|
◑ <b>{" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC</b> (Binance API)
|
||||||
@ -188,13 +187,13 @@ export default class BookPage extends Component {
|
|||||||
<Typography component="h5" variant="h5">
|
<Typography component="h5" variant="h5">
|
||||||
No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode}
|
No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography component="body1" variant="body1">
|
|
||||||
Be the first one to create an order
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
|
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Typography component="body1" variant="body1">
|
||||||
|
Be the first one to create an order
|
||||||
|
</Typography>
|
||||||
</Grid>)
|
</Grid>)
|
||||||
: this.bookCards()
|
: this.bookCards()
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
@ -37,7 +37,9 @@ export default class MakerPage extends Component {
|
|||||||
payment_method: this.defaultPaymentMethod,
|
payment_method: this.defaultPaymentMethod,
|
||||||
premium: 0,
|
premium: 0,
|
||||||
satoshis: null,
|
satoshis: null,
|
||||||
|
currencies_dict: {"1":"USD"}
|
||||||
}
|
}
|
||||||
|
this.getCurrencyDict()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTypeChange=(e)=>{
|
handleTypeChange=(e)=>{
|
||||||
@ -46,10 +48,9 @@ export default class MakerPage extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleCurrencyChange=(e)=>{
|
handleCurrencyChange=(e)=>{
|
||||||
var code = (e.target.value == 1 ) ? "USD": ((e.target.value == 2 ) ? "EUR":"ETH")
|
|
||||||
this.setState({
|
this.setState({
|
||||||
currency: e.target.value,
|
currency: e.target.value,
|
||||||
currencyCode: code,
|
currencyCode: this.getCurrencyCode(e.target.value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleAmountChange=(e)=>{
|
handleAmountChange=(e)=>{
|
||||||
@ -104,7 +105,22 @@ 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) :"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrencyDict() {
|
||||||
|
fetch('/api/currencies')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) =>
|
||||||
|
this.setState({
|
||||||
|
currencies_dict: data
|
||||||
|
}));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrencyCode(val){
|
||||||
|
return this.state.currencies_dict[val.toString()]
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -242,6 +258,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}
|
||||||
|
@ -68,6 +68,7 @@ export default class OrderPage extends Component {
|
|||||||
isBuyer:data.buyer,
|
isBuyer:data.buyer,
|
||||||
isSeller:data.seller,
|
isSeller:data.seller,
|
||||||
expiresAt:data.expires_at,
|
expiresAt:data.expires_at,
|
||||||
|
badRequest:data.bad_request,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -87,8 +88,10 @@ export default class OrderPage 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': getCookie('csrftoken')},
|
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({
|
||||||
|
'action':'take',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
|
@ -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())
|
||||||
|
5
setup.md
5
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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user