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
2022-01-18 16:57:55 +00:00
from django . template . defaultfilters import truncatechars
2022-01-04 13:47:37 +00:00
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 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-16 16:06:53 +00:00
class Currency ( models . Model ) :
2022-01-23 12:30:41 +00:00
currency_dict = json . load ( open ( ' frontend/static/assets/currencies.json ' ) )
2022-01-16 16:06:53 +00:00
currency_choices = [ ( int ( val ) , label ) for val , label in list ( currency_dict . items ( ) ) ]
currency = models . PositiveSmallIntegerField ( choices = currency_choices , null = False , unique = True )
exchange_rate = models . DecimalField ( max_digits = 10 , decimal_places = 2 , default = None , null = True , validators = [ MinValueValidator ( 0 ) ] )
timestamp = models . DateTimeField ( auto_now_add = True )
def __str__ ( self ) :
# returns currency label ( 3 letters code)
return self . currency_dict [ str ( self . currency ) ]
class Meta :
verbose_name = ' Cached market currency '
verbose_name_plural = ' Currencies '
2022-01-05 10:30:38 +00:00
class LNPayment ( models . Model ) :
class Types ( models . IntegerChoices ) :
2022-01-23 21:56:26 +00:00
NORM = 0 , ' Regular invoice '
2022-01-11 14:36:43 +00:00
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-17 16:41:55 +00:00
CANCEL = 4 , ' Cancelled '
EXPIRE = 5 , ' Expired '
VALIDI = 6 , ' Valid '
FLIGHT = 7 , ' In flight '
SUCCED = 8 , ' Succeeded '
FAILRO = 9 , ' Routing failed '
2022-01-05 10:30:38 +00:00
2022-01-06 12:32:17 +00:00
# payment use details
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
# payment info
2022-01-18 16:57:55 +00:00
payment_hash = models . CharField ( max_length = 100 , unique = True , default = None , blank = True , primary_key = True )
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
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-24 18:34:52 +00:00
cltv_expiry = models . PositiveSmallIntegerField ( null = True , default = None , blank = True )
2022-01-25 14:46:02 +00:00
expiry_height = models . PositiveBigIntegerField ( null = True , default = None , blank = True )
2022-01-05 10:30:38 +00:00
2022-01-23 21:56:26 +00:00
# routing
routing_attempts = models . PositiveSmallIntegerField ( null = False , default = 0 )
last_routing_time = models . DateTimeField ( null = True , default = None , blank = True )
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-18 16:57:55 +00:00
return ( f ' LN- { str ( self . payment_hash ) [ : 8 ] } : { self . Concepts ( self . concept ) . label } - { self . Status ( self . status ) . label } ' )
2022-01-01 22:34:23 +00:00
2022-01-16 16:06:53 +00:00
class Meta :
verbose_name = ' Lightning payment '
verbose_name_plural = ' Lightning payments '
2022-01-18 16:57:55 +00:00
@property
def hash ( self ) :
# Payment hash is the primary key of LNpayments
# However it is too long for the admin panel.
# We created a truncated property for display 'hash'
return truncatechars ( self . payment_hash , 10 )
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-16 21:54:42 +00:00
WFR = 16 , ' Wait for dispute resolution '
MLD = 17 , ' Maker lost dispute '
TLD = 18 , ' Taker lost dispute '
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-16 16:06:53 +00:00
currency = models . ForeignKey ( Currency , null = True , on_delete = models . SET_NULL )
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-20 17:30:29 +00:00
maker = models . ForeignKey ( User , related_name = ' maker ' , on_delete = models . SET_NULL , 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-30 19:45:37 +00:00
maker_last_seen = models . DateTimeField ( null = True , default = None , blank = True )
taker_last_seen = models . DateTimeField ( null = True , default = None , blank = True )
2022-01-23 19:02:25 +00:00
maker_asked_cancel = models . BooleanField ( default = False , null = False ) # When collaborative cancel is needed and one partner has cancelled.
taker_asked_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_fiat_sent = models . BooleanField ( default = False , null = False )
2022-01-06 20:33:40 +00:00
2022-01-16 21:54:42 +00:00
# in dispute
is_disputed = models . BooleanField ( default = False , null = False )
2022-01-18 16:57:55 +00:00
maker_statement = models . TextField ( max_length = 5000 , null = True , default = None , blank = True )
taker_statement = models . TextField ( max_length = 5000 , null = True , default = None , blank = True )
2022-01-16 21:54:42 +00:00
# LNpayments
2022-01-07 18:22:52 +00:00
# Order collateral
2022-01-17 23:11:41 +00:00
maker_bond = models . OneToOneField ( LNPayment , related_name = ' order_made ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
taker_bond = models . OneToOneField ( LNPayment , related_name = ' order_taken ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
trade_escrow = models . OneToOneField ( LNPayment , related_name = ' order_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-25 14:46:02 +00:00
payout = models . OneToOneField ( LNPayment , related_name = ' order_paid ' , on_delete = models . SET_NULL , null = True , default = None , blank = True )
2022-01-05 10:30:38 +00:00
2022-01-17 23:11:41 +00:00
# ratings
maker_rated = models . BooleanField ( default = False , null = False )
taker_rated = models . BooleanField ( default = False , null = False )
2022-01-07 18:22:52 +00:00
2022-01-16 21:54:42 +00:00
t_to_expire = {
2022-01-14 12:00:53 +00:00
0 : int ( config ( ' EXP_MAKER_BOND_INVOICE ' ) ) , # 'Waiting for maker bond'
1 : 60 * 60 * int ( config ( ' PUBLIC_ORDER_DURATION ' ) ) , # 'Public'
2 : 0 , # 'Deleted'
3 : int ( config ( ' EXP_TAKER_BOND_INVOICE ' ) ) , # 'Waiting for taker bond'
4 : 0 , # 'Cancelled'
5 : 0 , # 'Expired'
6 : 60 * int ( config ( ' INVOICE_AND_ESCROW_DURATION ' ) ) , # 'Waiting for trade collateral and buyer invoice'
7 : 60 * int ( config ( ' INVOICE_AND_ESCROW_DURATION ' ) ) , # 'Waiting only for seller trade collateral'
8 : 60 * int ( config ( ' INVOICE_AND_ESCROW_DURATION ' ) ) , # 'Waiting only for buyer invoice'
9 : 60 * 60 * int ( config ( ' FIAT_EXCHANGE_DURATION ' ) ) , # 'Sending fiat - In chatroom'
10 : 60 * 60 * int ( config ( ' FIAT_EXCHANGE_DURATION ' ) ) , # 'Fiat sent - In chatroom'
2022-01-23 12:30:41 +00:00
11 : 1 * 24 * 60 * 60 , # 'In dispute'
2022-01-14 12:00:53 +00:00
12 : 0 , # 'Collaboratively cancelled'
13 : 24 * 60 * 60 , # 'Sending satoshis to buyer'
14 : 24 * 60 * 60 , # 'Sucessful trade'
15 : 24 * 60 * 60 , # 'Failed lightning network routing'
2022-01-24 18:34:52 +00:00
16 : 10 * 24 * 60 * 60 , # 'Wait for dispute resolution'
2022-01-16 21:54:42 +00:00
17 : 24 * 60 * 60 , # 'Maker lost dispute'
18 : 24 * 60 * 60 , # 'Taker lost dispute'
2022-01-14 12:00:53 +00:00
}
2022-01-06 23:33:55 +00:00
def __str__ ( self ) :
2022-01-16 16:06:53 +00:00
return ( f ' Order { self . id } : { self . Types ( self . type ) . label } BTC for { float ( self . amount ) } { self . currency } ' )
2022-01-18 13:20:19 +00:00
2022-01-06 23:33:55 +00:00
2022-01-06 16:20:04 +00:00
@receiver ( pre_delete , sender = Order )
2022-01-16 12:31:25 +00:00
def delete_lnpayment_at_order_deletion ( sender , instance , * * kwargs ) :
2022-01-25 14:46:02 +00:00
to_delete = ( instance . maker_bond , instance . payout , instance . taker_bond , instance . trade_escrow )
2022-01-06 16:20:04 +00:00
2022-01-16 12:31:25 +00:00
for lnpayment in to_delete :
2022-01-06 16:20:04 +00:00
try :
2022-01-16 12:31:25 +00:00
lnpayment . delete ( )
2022-01-06 16:20:04 +00:00
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 )
2022-01-16 12:31:25 +00:00
# Total trades
total_contracts = models . PositiveIntegerField ( null = False , default = 0 )
2022-01-04 13:47:37 +00:00
# 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 )
2022-01-16 21:54:42 +00:00
num_disputes_started = models . PositiveIntegerField ( null = False , default = 0 )
orders_disputes_started = models . CharField ( max_length = 999 , null = True , default = None , validators = [ validate_comma_separated_integer_list ] , blank = True ) # Will only store ID of orders
2022-01-04 13:47:37 +00:00
# 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)
2022-01-26 11:44:45 +00:00
penalty_expiration = models . DateTimeField ( null = True , default = None , blank = True )
2022-01-10 12:10:32 +00:00
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 ) :
2022-01-16 12:31:25 +00:00
try :
avatar_file = Path ( ' frontend/ ' + instance . profile . avatar . url )
avatar_file . unlink ( )
except :
pass
2022-01-05 00:13:08 +00:00
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-16 21:54:42 +00:00
currency = models . ForeignKey ( Currency , null = True , on_delete = models . SET_NULL )
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-16 16:06:53 +00:00
market_exchange_rate = float ( order . currency . exchange_rate )
2022-01-16 15:18:23 +00:00
premium = 100 * ( price / market_exchange_rate - 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
2022-01-16 16:06:53 +00:00
class Meta :
verbose_name = ' Market tick '
verbose_name_plural = ' Market ticks '
2022-01-07 22:46:30 +00:00