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-06 16:54:37 +00:00
from decouple import config
2022-01-04 13:47:37 +00:00
from pathlib import Path
2022-01-06 12:32:17 +00:00
2022-01-01 22:34:23 +00:00
#############################
# TODO
# Load hparams from .env file
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-06 16:20:04 +00:00
2022-01-05 10:30:38 +00:00
class LNPayment ( models . Model ) :
class Types ( models . IntegerChoices ) :
NORM = 0 , ' Regular invoice ' # Only outgoing HTLCs will be regular invoices (Non-hodl)
HODL = 1 , ' Hodl invoice '
class Concepts ( models . IntegerChoices ) :
MAKEBOND = 0 , ' Maker bond '
TAKEBOND = 1 , ' Taker-buyer bond '
TRESCROW = 2 , ' Trade escrow '
PAYBUYER = 3 , ' Payment to buyer '
class Status ( models . IntegerChoices ) :
INVGEN = 0 , ' Hodl invoice was generated '
LOCKED = 1 , ' Hodl invoice has HTLCs locked '
2022-01-06 12:32:17 +00:00
SETLED = 2 , ' Invoice settled '
2022-01-05 10:30:38 +00:00
RETNED = 3 , ' Hodl invoice was returned '
MISSNG = 4 , ' Buyer invoice is missing '
2022-01-06 12:32:17 +00:00
VALIDI = 5 , ' Buyer invoice is valid '
INFAIL = 6 , ' Buyer invoice routing failed '
2022-01-05 10:30:38 +00:00
2022-01-06 12:32:17 +00:00
# payment use details
2022-01-05 10:30:38 +00:00
type = models . PositiveSmallIntegerField ( choices = Types . choices , null = False , default = Types . HODL )
concept = models . PositiveSmallIntegerField ( choices = Concepts . choices , null = False , default = Concepts . MAKEBOND )
status = models . PositiveSmallIntegerField ( choices = Status . choices , null = False , default = Status . INVGEN )
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-05 12:18:54 +00:00
invoice = models . CharField ( max_length = 300 , unique = False , null = True , default = None , blank = True )
2022-01-06 12:32:17 +00:00
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 )
2022-01-05 10:30:38 +00:00
expires_at = models . DateTimeField ( )
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-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 ) :
# Make relational back to ORDER
return ( f ' HTLC { self . id } : { self . Concepts ( self . concept ) . label } ' )
2022-01-01 22:34:23 +00:00
class Order ( models . Model ) :
class Types ( models . IntegerChoices ) :
BUY = 0 , ' BUY '
SELL = 1 , ' SELL '
class Currencies ( models . IntegerChoices ) :
USD = 1 , ' USD '
EUR = 2 , ' EUR '
ETH = 3 , ' ETH '
2022-01-05 10:30:38 +00:00
class Status ( models . IntegerChoices ) :
2022-01-06 13:55:47 +00:00
WFB = 0 , ' Waiting for maker bond '
PUB = 1 , ' Public '
DEL = 2 , ' Deleted '
2022-01-06 22:39:59 +00:00
TAK = 3 , ' Waiting for taker bond '
2022-01-06 20:33:40 +00:00
UCA = 4 , ' Cancelled '
WF2 = 5 , ' Waiting for trade collateral and buyer invoice '
2022-01-06 22:39:59 +00:00
WFE = 6 , ' Waiting only for seller trade collateral '
WFI = 7 , ' Waiting only for buyer invoice '
CHA = 8 , ' Sending fiat - In chatroom '
2022-01-06 20:33:40 +00:00
CCA = 9 , ' Collaboratively cancelled '
FSE = 10 , ' Fiat sent - In chatroom '
FCO = 11 , ' Fiat confirmed '
SUC = 12 , ' Sucessfully settled '
FAI = 13 , ' Failed lightning network routing '
UPI = 14 , ' Updated invoice '
DIS = 15 , ' In dispute '
MLD = 16 , ' Maker lost dispute '
TLD = 17 , ' Taker lost dispute '
EXP = 18 , ' Expired '
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 )
currency = models . PositiveSmallIntegerField ( choices = Currencies . choices , null = False )
2022-01-05 12:18:54 +00:00
amount = models . DecimalField ( max_digits = 9 , decimal_places = 4 , validators = [ MinValueValidator ( 0.00001 ) ] )
payment_method = models . CharField ( max_length = 30 , null = False , default = " not specified " , 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-01 22:34:23 +00:00
# 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-06 23:33:55 +00:00
def __str__ ( self ) :
# Make relational back to ORDER
return ( f ' Order { self . id } : { self . Types ( self . type ) . label } { " {:,} " . format ( self . t0_satoshis ) } Sats for { self . Currencies ( self . currency ) . label } ' )
2022-01-06 16:20:04 +00:00
@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
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
@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