2022-01-01 22:13:27 +00:00
from django . db import models
2022-01-01 22:34:23 +00:00
from django . contrib . auth . models import User
2022-01-04 13:47:37 +00:00
from django . core . validators import MaxValueValidator , MinValueValidator , validate_comma_separated_integer_list
from django . db . models . signals import post_save , pre_delete
from django . dispatch import receiver
2022-01-04 15:58:10 +00:00
from django . utils . html import mark_safe
2022-01-13 19:22:54 +00:00
import uuid
2022-01-04 15:58:10 +00:00
2022-01-06 16:54:37 +00:00
from decouple import config
2022-01-04 13:47:37 +00:00
from pathlib import Path
2022-01-07 22:46:30 +00:00
from . utils import get_exchange_rate
2022-01-07 23:48:23 +00:00
import json
2022-01-05 10:30:38 +00:00
2022-01-06 16:54:37 +00:00
MIN_TRADE = int ( config ( ' MIN_TRADE ' ) )
MAX_TRADE = int ( config ( ' MAX_TRADE ' ) )
FEE = float ( config ( ' FEE ' ) )
BOND_SIZE = float ( config ( ' BOND_SIZE ' ) )
2022-01-05 10:30:38 +00:00
class LNPayment ( models . Model ) :
class Types ( models . IntegerChoices ) :
2022-01-11 14:36:43 +00:00
NORM = 0 , ' Regular invoice ' # Only outgoing buyer payment will be a regular invoice (Non-hold)
HOLD = 1 , ' hold invoice '
2022-01-05 10:30:38 +00:00
class Concepts ( models . IntegerChoices ) :
MAKEBOND = 0 , ' Maker bond '
2022-01-07 11:31:33 +00:00
TAKEBOND = 1 , ' Taker bond '
2022-01-05 10:30:38 +00:00
TRESCROW = 2 , ' Trade escrow '
PAYBUYER = 3 , ' Payment to buyer '
class Status ( models . IntegerChoices ) :
2022-01-07 11:31:33 +00:00
INVGEN = 0 , ' Generated '
LOCKED = 1 , ' Locked '
SETLED = 2 , ' Settled '
RETNED = 3 , ' Returned '
2022-01-12 00:02:17 +00:00
EXPIRE = 4 , ' Expired '
2022-01-07 11:31:33 +00:00
VALIDI = 5 , ' Valid '
2022-01-12 12:57:03 +00:00
FLIGHT = 6 , ' In flight '
SUCCED = 7 , ' Succeeded '
FAILRO = 8 , ' Routing failed '
2022-01-05 10:30:38 +00:00
2022-01-06 12:32:17 +00:00
# payment use details
2022-01-13 19:22:54 +00:00
id = models . UUIDField ( primary_key = True , default = uuid . uuid4 , editable = False )
2022-01-11 14:36:43 +00:00
type = models . PositiveSmallIntegerField ( choices = Types . choices , null = False , default = Types . HOLD )
2022-01-05 10:30:38 +00:00
concept = models . PositiveSmallIntegerField ( choices = Concepts . choices , null = False , default = Concepts . MAKEBOND )
status = models . PositiveSmallIntegerField ( choices = Status . choices , null = False , default = Status . INVGEN )
2022-01-06 12:32:17 +00:00
routing_retries = models . PositiveSmallIntegerField ( null = False , default = 0 )
2022-01-05 10:30:38 +00:00
2022-01-06 12:32:17 +00:00
# payment info
2022-01-12 12:57:03 +00:00
invoice = models . CharField ( max_length = 1200 , unique = True , null = True , default = None , blank = True ) # Some invoices with lots of routing hints might be long
2022-01-11 01:02:06 +00:00
payment_hash = models . CharField ( max_length = 100 , unique = True , null = True , default = None , blank = True )
preimage = models . CharField ( max_length = 64 , unique = True , null = True , default = None , blank = True )
2022-01-12 12:57:03 +00:00
description = models . CharField ( max_length = 500 , unique = False , null = True , default = None , blank = True )
2022-01-06 12:32:17 +00:00
num_satoshis = models . PositiveBigIntegerField ( validators = [ MinValueValidator ( MIN_TRADE * BOND_SIZE ) , MaxValueValidator ( MAX_TRADE * ( 1 + BOND_SIZE + FEE ) ) ] )
2022-01-11 01:02:06 +00:00
created_at = models . DateTimeField ( )
expires_at = models . DateTimeField ( )
2022-01-05 10:30:38 +00:00
2022-01-06 12:32:17 +00:00
# involved parties
2022-01-05 10:30:38 +00:00
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 )
2022-01-05 12:18:54 +00:00
def __str__ ( self ) :
2022-01-13 19:22:54 +00:00
return ( f ' LN- { str ( self . id ) [ : 8 ] } : { self . Concepts ( self . concept ) . label } - { self . Status ( self . status ) . label } ' )
2022-01-01 22:34:23 +00:00
class Order ( models . Model ) :
class Types ( models . IntegerChoices ) :
BUY = 0 , ' BUY '
SELL = 1 , ' SELL '
2022-01-05 10:30:38 +00:00
class Status ( models . IntegerChoices ) :
2022-01-07 11:31:33 +00:00
WFB = 0 , ' Waiting for maker bond '
PUB = 1 , ' Public '
DEL = 2 , ' Deleted '
TAK = 3 , ' Waiting for taker bond '
UCA = 4 , ' Cancelled '
2022-01-07 18:22:52 +00:00
EXP = 5 , ' Expired '
WF2 = 6 , ' Waiting for trade collateral and buyer invoice '
WFE = 7 , ' Waiting only for seller trade collateral '
WFI = 8 , ' Waiting only for buyer invoice '
CHA = 9 , ' Sending fiat - In chatroom '
2022-01-06 20:33:40 +00:00
FSE = 10 , ' Fiat sent - In chatroom '
2022-01-07 18:22:52 +00:00
DIS = 11 , ' In dispute '
CCA = 12 , ' Collaboratively cancelled '
PAY = 13 , ' Sending satoshis to buyer '
2022-01-12 00:02:17 +00:00
SUC = 14 , ' Sucessful trade '
2022-01-07 18:22:52 +00:00
FAI = 15 , ' Failed lightning network routing '
2022-01-06 20:33:40 +00:00
MLD = 16 , ' Maker lost dispute '
TLD = 17 , ' Taker lost dispute '
2022-01-09 01:18:11 +00:00
2022-01-09 14:29:10 +00:00
currency_dict = json . load ( open ( ' ./frontend/static/assets/currencies.json ' ) )
2022-01-07 23:48:23 +00:00
currency_choices = [ ( int ( val ) , label ) for val , label in list ( currency_dict . items ( ) ) ]
2022-01-01 22:34:23 +00:00
2022-01-05 00:13:08 +00:00
# order info
2022-01-05 10:30:38 +00:00
status = models . PositiveSmallIntegerField ( choices = Status . choices , null = False , default = Status . WFB )
2022-01-01 22:34:23 +00:00
created_at = models . DateTimeField ( auto_now_add = True )
2022-01-04 10:21:45 +00:00
expires_at = models . DateTimeField ( )
2022-01-01 22:34:23 +00:00
# order details
type = models . PositiveSmallIntegerField ( choices = Types . choices , null = False )
2022-01-07 23:48:23 +00:00
currency = models . PositiveSmallIntegerField ( choices = currency_choices , null = False )
2022-01-05 12:18:54 +00:00
amount = models . DecimalField ( max_digits = 9 , decimal_places = 4 , validators = [ MinValueValidator ( 0.00001 ) ] )
2022-01-08 00:29:04 +00:00
payment_method = models . CharField ( max_length = 35 , null = False , default = " not specified " , blank = True )
2022-01-02 13:24:35 +00:00
2022-01-05 10:30:38 +00:00
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
is_explicit = models . BooleanField ( default = False , null = False )
2022-01-06 13:55:47 +00:00
# marked to market
2022-01-05 12:18:54 +00:00
premium = models . DecimalField ( max_digits = 5 , decimal_places = 2 , default = 0 , null = True , validators = [ MinValueValidator ( - 100 ) , MaxValueValidator ( 999 ) ] , blank = True )
2022-01-05 10:30:38 +00:00
# explicit
2022-01-05 12:18:54 +00:00
satoshis = models . PositiveBigIntegerField ( null = True , validators = [ MinValueValidator ( MIN_TRADE ) , MaxValueValidator ( MAX_TRADE ) ] , blank = True )
2022-01-06 13:55:47 +00:00
# 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...
2022-01-01 22:34:23 +00:00
# order participants
2022-01-04 13:47:37 +00:00
maker = models . ForeignKey ( User , related_name = ' maker ' , on_delete = models . CASCADE , null = True , default = None ) # unique = True, a maker can only make one order
2022-01-05 12:18:54 +00:00
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
2022-01-06 20:33:40 +00:00
is_pending_cancel = models . BooleanField ( default = False , null = False ) # When collaborative cancel is needed and one partner has cancelled.
2022-01-07 18:22:52 +00:00
is_disputed = models . BooleanField ( default = False , null = False )
is_fiat_sent = models . BooleanField ( default = False , null = False )
2022-01-06 20:33:40 +00:00
2022-01-07 18:22:52 +00:00
# HTLCs
# Order collateral
2022-01-05 12:18:54 +00:00
maker_bond = models . ForeignKey ( LNPayment , related_name = ' maker_bond ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
taker_bond = models . ForeignKey ( LNPayment , related_name = ' taker_bond ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
trade_escrow = models . ForeignKey ( LNPayment , related_name = ' trade_escrow ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
2022-01-01 22:34:23 +00:00
# buyer payment LN invoice
2022-01-05 12:18:54 +00:00
buyer_invoice = models . ForeignKey ( LNPayment , related_name = ' buyer_invoice ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
2022-01-05 10:30:38 +00:00
2022-01-07 18:22:52 +00:00
# cancel LN invoice // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
maker_cancel = models . ForeignKey ( LNPayment , related_name = ' maker_cancel ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
taker_cancel = models . ForeignKey ( LNPayment , related_name = ' taker_cancel ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
2022-01-06 23:33:55 +00:00
def __str__ ( self ) :
# Make relational back to ORDER
2022-01-09 01:18:11 +00:00
return ( f ' Order { self . id } : { self . Types ( self . type ) . label } BTC for { float ( self . amount ) } { self . currency_dict [ str ( self . currency ) ] } ' )
2022-01-06 23:33:55 +00:00
2022-01-06 16:20:04 +00:00
@receiver ( pre_delete , sender = Order )
2022-01-09 20:05:19 +00:00
def delete_HTLCs_at_order_deletion ( sender , instance , * * kwargs ) :
2022-01-06 16:20:04 +00:00
to_delete = ( instance . maker_bond , instance . buyer_invoice , instance . taker_bond , instance . trade_escrow )
for htlc in to_delete :
try :
htlc . delete ( )
except :
pass
2022-01-04 13:47:37 +00:00
class Profile ( models . Model ) :
2022-01-05 00:13:08 +00:00
2022-01-04 13:47:37 +00:00
user = models . OneToOneField ( User , on_delete = models . CASCADE )
# Ratings stored as a comma separated integer list
total_ratings = models . PositiveIntegerField ( null = False , default = 0 )
2022-01-05 12:18:54 +00:00
latest_ratings = models . CharField ( max_length = 999 , null = True , default = None , validators = [ validate_comma_separated_integer_list ] , blank = True ) # Will only store latest ratings
avg_rating = models . DecimalField ( max_digits = 4 , decimal_places = 1 , default = None , null = True , validators = [ MinValueValidator ( 0 ) , MaxValueValidator ( 100 ) ] , blank = True )
2022-01-04 13:47:37 +00:00
# Disputes
num_disputes = models . PositiveIntegerField ( null = False , default = 0 )
lost_disputes = models . PositiveIntegerField ( null = False , default = 0 )
# RoboHash
2022-01-05 12:18:54 +00:00
avatar = models . ImageField ( default = " static/assets/misc/unknown_avatar.png " , verbose_name = ' Avatar ' , blank = True )
2022-01-04 13:47:37 +00:00
2022-01-10 12:10:32 +00:00
# Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond)
penalty_expiration = models . DateTimeField ( null = True )
2022-01-04 13:47:37 +00:00
@receiver ( post_save , sender = User )
def create_user_profile ( sender , instance , created , * * kwargs ) :
if created :
Profile . objects . create ( user = instance )
@receiver ( post_save , sender = User )
def save_user_profile ( sender , instance , * * kwargs ) :
instance . profile . save ( )
2022-01-04 15:58:10 +00:00
2022-01-05 00:13:08 +00:00
@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
2022-01-04 15:58:10 +00:00
def __str__ ( self ) :
return self . user . username
2022-01-04 13:47:37 +00:00
2022-01-04 15:58:10 +00:00
# to display avatars in admin panel
def get_avatar ( self ) :
if not self . avatar :
2022-01-05 00:13:08 +00:00
return ' static/assets/misc/unknown_avatar.png '
2022-01-04 15:58:10 +00:00
return self . avatar . url
# method to create a fake table field in read only mode
def avatar_tag ( self ) :
return mark_safe ( ' <img src= " %s " width= " 50 " height= " 50 " /> ' % self . get_avatar ( ) )
2022-01-06 12:32:17 +00:00
2022-01-07 22:46:30 +00:00
class MarketTick ( models . Model ) :
'''
Records tick by tick Non - KYC Bitcoin price .
Data to be aggregated and offered via public API .
2022-01-08 13:08:03 +00:00
It is checked against current CEX price for useful
2022-01-07 22:46:30 +00:00
insight on the historical premium of Non - KYC BTC
2022-01-09 20:05:19 +00:00
Price is set when taker bond is locked . Both
2022-01-07 22:46:30 +00:00
maker and taker are commited with bonds ( contract
is finished and cancellation has a cost )
'''
2022-01-13 19:22:54 +00:00
id = models . UUIDField ( primary_key = True , default = uuid . uuid4 , editable = False )
2022-01-07 22:46:30 +00:00
price = models . DecimalField ( max_digits = 10 , decimal_places = 2 , default = None , null = True , validators = [ MinValueValidator ( 0 ) ] )
volume = models . DecimalField ( max_digits = 8 , decimal_places = 8 , default = None , null = True , validators = [ MinValueValidator ( 0 ) ] )
premium = models . DecimalField ( max_digits = 5 , decimal_places = 2 , default = None , null = True , validators = [ MinValueValidator ( - 100 ) , MaxValueValidator ( 999 ) ] , blank = True )
2022-01-09 01:18:11 +00:00
currency = models . PositiveSmallIntegerField ( choices = Order . currency_choices , null = True )
2022-01-07 22:46:30 +00:00
timestamp = models . DateTimeField ( auto_now_add = True )
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
fee = models . DecimalField ( max_digits = 4 , decimal_places = 4 , default = FEE , validators = [ MinValueValidator ( 0 ) , MaxValueValidator ( 1 ) ] )
def log_a_tick ( order ) :
'''
2022-01-08 00:29:04 +00:00
Creates a new tick
2022-01-07 22:46:30 +00:00
'''
2022-01-08 00:29:04 +00:00
2022-01-07 22:46:30 +00:00
if not order . taker_bond :
return None
elif order . taker_bond . status == LNPayment . Status . LOCKED :
volume = order . last_satoshis / 100000000
price = float ( order . amount ) / volume # Amount Fiat / Amount BTC
2022-01-09 01:18:11 +00:00
premium = 100 * ( price / get_exchange_rate ( Order . currency_dict [ str ( order . currency ) ] ) - 1 )
2022-01-07 22:46:30 +00:00
tick = MarketTick . objects . create (
price = price ,
volume = volume ,
premium = premium ,
currency = order . currency )
2022-01-08 00:29:04 +00:00
2022-01-07 22:46:30 +00:00
tick . save ( )
def __str__ ( self ) :
2022-01-13 19:22:54 +00:00
return f ' Tick: { str ( self . id ) [ : 8 ] } '
2022-01-07 22:46:30 +00:00