2022-01-17 23:11:41 +00:00
from datetime import timedelta
2022-01-06 16:54:37 +00:00
from django . utils import timezone
2022-01-17 23:11:41 +00:00
from api . lightning . node import LNNode
2022-02-03 21:51:42 +00:00
from django . db . models import Q
2022-01-06 16:54:37 +00:00
2022-01-17 23:11:41 +00:00
from api . models import Order , LNPayment , MarketTick , User , Currency
2022-02-23 16:15:48 +00:00
from api . tasks import send_message
2022-01-06 16:54:37 +00:00
from decouple import config
2022-01-11 14:36:43 +00:00
import math
2022-01-17 23:11:41 +00:00
import ast
2022-03-14 17:57:06 +00:00
import time
2022-01-11 14:36:43 +00:00
2022-02-17 19:50:10 +00:00
FEE = float ( config ( " FEE " ) )
2022-03-03 15:40:56 +00:00
MAKER_FEE_SPLIT = float ( config ( " MAKER_FEE_SPLIT " ) )
2022-02-17 19:50:10 +00:00
ESCROW_USERNAME = config ( " ESCROW_USERNAME " )
PENALTY_TIMEOUT = int ( config ( " PENALTY_TIMEOUT " ) )
2022-01-06 16:54:37 +00:00
2022-02-17 19:50:10 +00:00
MIN_TRADE = int ( config ( " MIN_TRADE " ) )
MAX_TRADE = int ( config ( " MAX_TRADE " ) )
2022-01-06 21:36:22 +00:00
2022-02-17 19:50:10 +00:00
EXP_MAKER_BOND_INVOICE = int ( config ( " EXP_MAKER_BOND_INVOICE " ) )
EXP_TAKER_BOND_INVOICE = int ( config ( " EXP_TAKER_BOND_INVOICE " ) )
2022-01-06 20:33:40 +00:00
2022-02-17 19:50:10 +00:00
BOND_EXPIRY = int ( config ( " BOND_EXPIRY " ) )
ESCROW_EXPIRY = int ( config ( " ESCROW_EXPIRY " ) )
2022-01-06 16:54:37 +00:00
2022-02-17 19:50:10 +00:00
INVOICE_AND_ESCROW_DURATION = int ( config ( " INVOICE_AND_ESCROW_DURATION " ) )
FIAT_EXCHANGE_DURATION = int ( config ( " FIAT_EXCHANGE_DURATION " ) )
2022-01-10 12:10:32 +00:00
2022-02-17 19:50:10 +00:00
class Logics :
2022-02-03 21:51:42 +00:00
@classmethod
def validate_already_maker_or_taker ( cls , user ) :
2022-02-17 19:50:10 +00:00
""" Validates if a use is already not part of an active order """
active_order_status = [
Order . Status . WFB ,
Order . Status . PUB ,
Order . Status . TAK ,
Order . Status . WF2 ,
Order . Status . WFE ,
Order . Status . WFI ,
Order . Status . CHA ,
Order . Status . FSE ,
Order . Status . DIS ,
Order . Status . WFR ,
]
""" Checks if the user is already partipant of an active order """
queryset = Order . objects . filter ( maker = user ,
status__in = active_order_status )
2022-01-06 16:54:37 +00:00
if queryset . exists ( ) :
2022-02-17 19:50:10 +00:00
return (
False ,
{
" bad_request " : " You are already maker of an active order "
} ,
queryset [ 0 ] ,
)
2022-01-17 23:22:44 +00:00
2022-02-17 19:50:10 +00:00
queryset = Order . objects . filter ( taker = user ,
status__in = active_order_status )
2022-01-06 16:54:37 +00:00
if queryset . exists ( ) :
2022-02-17 19:50:10 +00:00
return (
False ,
{
" bad_request " : " You are already taker of an active order "
} ,
queryset [ 0 ] ,
)
2022-02-03 21:51:42 +00:00
# Edge case when the user is in an order that is failing payment and he is the buyer
2022-02-17 19:50:10 +00:00
queryset = Order . objects . filter ( Q ( maker = user ) | Q ( taker = user ) ,
status = Order . Status . FAI )
2022-02-03 21:51:42 +00:00
if queryset . exists ( ) :
order = queryset [ 0 ]
if cls . is_buyer ( order , user ) :
2022-02-17 19:50:10 +00:00
return (
False ,
{
" bad_request " :
" You are still pending a payment from a recent order "
} ,
order ,
)
2022-02-03 21:51:42 +00:00
2022-01-29 19:51:26 +00:00
return True , None , None
2022-01-06 16:54:37 +00:00
2022-03-21 23:27:36 +00:00
@classmethod
def validate_order_size ( cls , order ) :
""" Validates if order size in Sats is within limits at t0 """
if not order . has_range :
if order . t0_satoshis > MAX_TRADE :
return False , {
" bad_request " :
" Your order is too big. It is worth " +
" {:,} " . format ( order . t0_satoshis ) +
" Sats now, but the limit is " + " {:,} " . format ( MAX_TRADE ) +
" Sats "
}
if order . t0_satoshis < MIN_TRADE :
return False , {
" bad_request " :
" Your order is too small. It is worth " +
" {:,} " . format ( order . t0_satoshis ) +
" Sats now, but the limit is " + " {:,} " . format ( MIN_TRADE ) +
" Sats "
}
elif order . has_range :
min_sats = cls . calc_sats ( order . min_amount , order . currency . exchange_rate , order . premium )
max_sats = cls . calc_sats ( order . max_amount , order . currency . exchange_rate , order . premium )
if min_sats > max_sats / 1.5 :
return False , {
" bad_request " :
2022-03-24 15:43:31 +00:00
" max amount must be at least 50 percent larger than min amount "
2022-03-21 23:27:36 +00:00
}
elif max_sats > MAX_TRADE :
return False , {
" bad_request " :
2022-03-22 17:49:57 +00:00
" Your order maximum amount is too big. It is worth " +
" {:,} " . format ( int ( max_sats ) ) +
2022-03-21 23:27:36 +00:00
" Sats now, but the limit is " + " {:,} " . format ( MAX_TRADE ) +
" Sats "
}
elif min_sats < MIN_TRADE :
return False , {
" bad_request " :
2022-03-22 17:49:57 +00:00
" Your order minimum amount is too small. It is worth " +
" {:,} " . format ( int ( min_sats ) ) +
2022-03-24 15:43:31 +00:00
" Sats now, but the limit is " + " {:,} " . format ( MIN_TRADE ) +
2022-03-21 23:27:36 +00:00
" Sats "
}
elif min_sats < max_sats / 5 :
return False , {
" bad_request " :
f " Your order amount range is too large. Max amount can only be 5 times bigger than min amount "
}
2022-01-06 21:36:22 +00:00
return True , None
2022-01-10 12:10:32 +00:00
2022-03-22 17:49:57 +00:00
def validate_amount_within_range ( order , amount ) :
if amount > float ( order . max_amount ) or amount < float ( order . min_amount ) :
return False , {
" bad_request " :
" The amount specified is outside the range specified by the maker "
}
return True , None
2022-02-03 18:06:30 +00:00
def user_activity_status ( last_seen ) :
if last_seen > ( timezone . now ( ) - timedelta ( minutes = 2 ) ) :
2022-02-17 19:50:10 +00:00
return " Active "
2022-02-03 18:06:30 +00:00
elif last_seen > ( timezone . now ( ) - timedelta ( minutes = 10 ) ) :
2022-02-17 19:50:10 +00:00
return " Seen recently "
2022-02-03 18:06:30 +00:00
else :
2022-02-17 19:50:10 +00:00
return " Inactive "
2022-02-03 18:06:30 +00:00
2022-02-17 19:50:10 +00:00
@classmethod
2022-03-22 17:49:57 +00:00
def take ( cls , order , user , amount = None ) :
2022-01-10 12:10:32 +00:00
is_penalized , time_out = cls . is_penalized ( user )
if is_penalized :
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " ,
f " You need to wait { time_out } seconds to take an order " ,
}
2022-01-10 12:10:32 +00:00
else :
2022-03-22 17:49:57 +00:00
if order . has_range :
order . amount = amount
2022-01-10 12:10:32 +00:00
order . taker = user
order . status = Order . Status . TAK
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . TAK ) )
2022-01-10 12:10:32 +00:00
order . save ( )
2022-03-11 15:24:39 +00:00
# send_message.delay(order.id,'order_taken') # Too spammy
2022-01-10 12:10:32 +00:00
return True , None
2022-01-06 16:54:37 +00:00
def is_buyer ( order , user ) :
is_maker = order . maker == user
is_taker = order . taker == user
2022-02-17 19:50:10 +00:00
return ( is_maker and order . type == Order . Types . BUY ) or (
is_taker and order . type == Order . Types . SELL )
2022-01-06 16:54:37 +00:00
def is_seller ( order , user ) :
is_maker = order . maker == user
is_taker = order . taker == user
2022-02-17 19:50:10 +00:00
return ( is_maker and order . type == Order . Types . SELL ) or (
is_taker and order . type == Order . Types . BUY )
2022-03-21 23:27:36 +00:00
def calc_sats ( amount , exchange_rate , premium ) :
exchange_rate = float ( exchange_rate )
premium_rate = exchange_rate * ( 1 + float ( premium ) / 100 )
return ( float ( amount ) / premium_rate ) * 100 * 1000 * 1000
@classmethod
def satoshis_now ( cls , order ) :
2022-02-17 19:50:10 +00:00
""" checks trade amount in sats """
2022-01-06 16:54:37 +00:00
if order . is_explicit :
satoshis_now = order . satoshis
else :
2022-03-21 23:27:36 +00:00
amount = order . amount if order . amount != None else order . max_amount
satoshis_now = cls . calc_sats ( amount , order . currency . exchange_rate , order . premium )
2022-01-06 16:54:37 +00:00
2022-01-07 11:31:33 +00:00
return int ( satoshis_now )
2022-01-10 01:12:58 +00:00
def price_and_premium_now ( order ) :
2022-02-17 19:50:10 +00:00
""" computes order price and premium with current rates """
2022-01-16 16:06:53 +00:00
exchange_rate = float ( order . currency . exchange_rate )
2022-01-10 01:12:58 +00:00
if not order . is_explicit :
premium = order . premium
2022-02-17 19:50:10 +00:00
price = exchange_rate * ( 1 + float ( premium ) / 100 )
2022-01-10 01:12:58 +00:00
else :
2022-02-17 19:50:10 +00:00
order_rate = float (
order . amount ) / ( float ( order . satoshis ) / 100000000 )
2022-01-10 01:12:58 +00:00
premium = order_rate / exchange_rate - 1
2022-02-17 19:50:10 +00:00
premium = int ( premium * 10000 ) / 100 # 2 decimals left
2022-01-10 01:12:58 +00:00
price = order_rate
2022-01-11 14:36:43 +00:00
2022-01-14 21:40:54 +00:00
significant_digits = 5
2022-02-17 19:50:10 +00:00
price = round (
price ,
significant_digits - int ( math . floor ( math . log10 ( abs ( price ) ) ) ) - 1 )
2022-01-10 01:12:58 +00:00
return price , premium
2022-01-16 18:32:34 +00:00
@classmethod
def order_expires ( cls , order ) :
2022-02-17 19:50:10 +00:00
""" General cases when time runs out. """
2022-01-16 18:32:34 +00:00
2022-01-16 21:54:42 +00:00
# Do not change order status if an order in any with
# any of these status is sent to expire here
2022-02-17 19:50:10 +00:00
does_not_expire = [
Order . Status . DEL ,
Order . Status . UCA ,
Order . Status . EXP ,
Order . Status . TLD ,
Order . Status . DIS ,
Order . Status . CCA ,
Order . Status . PAY ,
Order . Status . SUC ,
Order . Status . FAI ,
Order . Status . MLD ,
]
2022-01-16 21:54:42 +00:00
2022-01-20 20:50:25 +00:00
if order . status in does_not_expire :
2022-01-16 21:54:42 +00:00
return False
elif order . status == Order . Status . WFB :
2022-01-16 18:32:34 +00:00
order . status = Order . Status . EXP
2022-01-17 18:11:44 +00:00
cls . cancel_bond ( order . maker_bond )
2022-01-16 18:32:34 +00:00
order . save ( )
2022-01-16 21:54:42 +00:00
return True
2022-02-17 19:50:10 +00:00
2022-01-16 21:54:42 +00:00
elif order . status == Order . Status . PUB :
2022-01-16 18:32:34 +00:00
cls . return_bond ( order . maker_bond )
order . status = Order . Status . EXP
order . save ( )
2022-02-23 16:15:48 +00:00
send_message . delay ( order . id , ' order_expired_untaken ' )
2022-01-16 21:54:42 +00:00
return True
elif order . status == Order . Status . TAK :
2022-01-17 18:11:44 +00:00
cls . cancel_bond ( order . taker_bond )
2022-01-16 21:54:42 +00:00
cls . kick_taker ( order )
2022-03-11 15:24:39 +00:00
# send_message.delay(order.id,'taker_expired_b4bond') # Too spammy
2022-01-16 21:54:42 +00:00
return True
elif order . status == Order . Status . WF2 :
2022-02-17 19:50:10 +00:00
""" Weird case where an order expires and both participants
2022-01-16 21:54:42 +00:00
did not proceed with the contract . Likely the site was
down or there was a bug . Still bonds must be charged
2022-02-17 19:50:10 +00:00
to avoid service DDOS . """
2022-01-16 21:54:42 +00:00
cls . settle_bond ( order . maker_bond )
cls . settle_bond ( order . taker_bond )
2022-01-17 23:11:41 +00:00
cls . cancel_escrow ( order )
2022-01-16 21:54:42 +00:00
order . status = Order . Status . EXP
order . save ( )
return True
elif order . status == Order . Status . WFE :
maker_is_seller = cls . is_seller ( order , order . maker )
# If maker is seller, settle the bond and order goes to expired
if maker_is_seller :
cls . settle_bond ( order . maker_bond )
2022-01-17 18:11:44 +00:00
cls . return_bond ( order . taker_bond )
2022-02-21 10:05:19 +00:00
# If seller is offline the escrow LNpayment does not exist
try :
2022-02-20 14:38:29 +00:00
cls . cancel_escrow ( order )
except :
pass
2022-01-16 21:54:42 +00:00
order . status = Order . Status . EXP
order . save ( )
2022-03-07 21:46:52 +00:00
# Reward taker with part of the maker bond
cls . add_slashed_rewards ( order . maker_bond , order . taker . profile )
2022-01-16 21:54:42 +00:00
return True
# If maker is buyer, settle the taker's bond order goes back to public
else :
cls . settle_bond ( order . taker_bond )
2022-02-21 10:05:19 +00:00
# If seller is offline the escrow LNpayment does not even exist
try :
2022-02-20 14:38:29 +00:00
cls . cancel_escrow ( order )
except :
pass
2022-03-07 21:46:52 +00:00
taker_bond = order . taker_bond
2022-01-16 21:54:42 +00:00
order . taker = None
order . taker_bond = None
2022-01-17 23:11:41 +00:00
order . trade_escrow = None
2022-01-18 15:23:57 +00:00
cls . publish_order ( order )
2022-03-11 15:55:55 +00:00
send_message . delay ( order . id , ' order_published ' )
2022-03-07 21:46:52 +00:00
# Reward maker with part of the taker bond
cls . add_slashed_rewards ( taker_bond , order . maker . profile )
2022-01-16 21:54:42 +00:00
return True
elif order . status == Order . Status . WFI :
# The trade could happen without a buyer invoice. However, this user
2022-01-17 23:11:41 +00:00
# is likely AFK; will probably desert the contract as well.
2022-01-16 21:54:42 +00:00
maker_is_buyer = cls . is_buyer ( order , order . maker )
# If maker is buyer, settle the bond and order goes to expired
if maker_is_buyer :
cls . settle_bond ( order . maker_bond )
2022-01-17 18:11:44 +00:00
cls . return_bond ( order . taker_bond )
2022-01-17 23:11:41 +00:00
cls . return_escrow ( order )
2022-01-16 21:54:42 +00:00
order . status = Order . Status . EXP
order . save ( )
2022-03-07 21:46:52 +00:00
# Reward taker with part of the maker bond
cls . add_slashed_rewards ( order . maker_bond , order . taker . profile )
2022-01-16 21:54:42 +00:00
return True
2022-01-17 18:11:44 +00:00
# If maker is seller settle the taker's bond, order goes back to public
2022-01-16 21:54:42 +00:00
else :
cls . settle_bond ( order . taker_bond )
2022-01-17 23:11:41 +00:00
cls . return_escrow ( order )
2022-03-07 21:46:52 +00:00
taker_bond = order . taker_bond
2022-01-16 21:54:42 +00:00
order . taker = None
order . taker_bond = None
2022-01-17 23:11:41 +00:00
order . trade_escrow = None
2022-01-18 15:23:57 +00:00
cls . publish_order ( order )
2022-03-11 15:55:55 +00:00
send_message . delay ( order . id , ' order_published ' )
2022-03-07 21:46:52 +00:00
# Reward maker with part of the taker bond
cls . add_slashed_rewards ( taker_bond , order . maker . profile )
2022-01-16 21:54:42 +00:00
return True
2022-02-17 19:50:10 +00:00
2022-01-19 20:55:24 +00:00
elif order . status in [ Order . Status . CHA , Order . Status . FSE ] :
# Another weird case. The time to confirm 'fiat sent or received' expired. Yet no dispute
2022-02-17 19:50:10 +00:00
# was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
2022-01-17 23:11:41 +00:00
# sent", we assume this is a dispute case by default.
2022-01-16 21:54:42 +00:00
cls . open_dispute ( order )
return True
2022-01-06 16:54:37 +00:00
2022-01-18 15:23:57 +00:00
@classmethod
def kick_taker ( cls , order ) :
2022-02-17 19:50:10 +00:00
""" The taker did not lock the taker_bond. Now he has to go """
2022-01-12 00:02:17 +00:00
# Add a time out to the taker
2022-01-18 18:40:56 +00:00
if order . taker :
profile = order . taker . profile
2022-02-17 19:50:10 +00:00
profile . penalty_expiration = timezone . now ( ) + timedelta (
seconds = PENALTY_TIMEOUT )
2022-01-18 18:40:56 +00:00
profile . save ( )
2022-01-12 00:02:17 +00:00
2022-01-17 18:11:44 +00:00
# Make order public again
order . taker = None
order . taker_bond = None
2022-01-18 15:23:57 +00:00
cls . publish_order ( order )
2022-01-17 18:11:44 +00:00
return True
2022-01-12 00:02:17 +00:00
2022-01-16 21:54:42 +00:00
@classmethod
def open_dispute ( cls , order , user = None ) :
2022-03-07 21:46:52 +00:00
# Always settle escrow and bonds during a dispute. Disputes
2022-02-20 11:39:28 +00:00
# can take long to resolve, it might trigger force closure
2022-03-07 21:46:52 +00:00
# for unresolved HTLCs) Dispute winner will have to submit a
2022-02-20 11:39:28 +00:00
# new invoice for value of escrow + bond.
2022-01-17 23:11:41 +00:00
2022-01-16 21:54:42 +00:00
if not order . trade_escrow . status == LNPayment . Status . SETLED :
2022-02-17 19:50:10 +00:00
cls . settle_escrow ( order )
2022-02-20 11:39:28 +00:00
cls . settle_bond ( order . maker_bond )
cls . settle_bond ( order . taker_bond )
2022-02-17 19:50:10 +00:00
2022-01-16 21:54:42 +00:00
order . is_disputed = True
order . status = Order . Status . DIS
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . DIS ) )
2022-01-16 21:54:42 +00:00
order . save ( )
# User could be None if a dispute is open automatically due to weird expiration.
if not user == None :
profile = user . profile
profile . num_disputes = profile . num_disputes + 1
2022-01-20 17:30:29 +00:00
if profile . orders_disputes_started == None :
profile . orders_disputes_started = [ str ( order . id ) ]
else :
2022-02-17 19:50:10 +00:00
profile . orders_disputes_started = list (
profile . orders_disputes_started ) . append ( str ( order . id ) )
2022-01-16 21:54:42 +00:00
profile . save ( )
return True , None
2022-01-17 23:11:41 +00:00
2022-01-16 21:54:42 +00:00
def dispute_statement ( order , user , statement ) :
2022-02-17 19:50:10 +00:00
""" Updates the dispute statements """
2022-01-27 14:40:14 +00:00
2022-01-17 16:41:55 +00:00
if not order . status == Order . Status . DIS :
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " :
2022-02-24 20:47:46 +00:00
" Only orders in dispute accept dispute statements "
2022-02-17 19:50:10 +00:00
}
2022-01-16 21:54:42 +00:00
if len ( statement ) > 5000 :
2022-02-17 19:50:10 +00:00
return False , {
" bad_statement " : " The statement is longer than 5000 characters "
}
2022-02-24 20:47:46 +00:00
if len ( statement ) < 100 :
return False , {
" bad_statement " : " The statement is too short. Make sure to be thorough. "
}
2022-01-17 16:41:55 +00:00
2022-01-16 21:54:42 +00:00
if order . maker == user :
order . maker_statement = statement
else :
order . taker_statement = statement
2022-02-17 19:50:10 +00:00
2022-01-17 16:41:55 +00:00
# If both statements are in, move status to wait for dispute resolution
2022-02-24 21:59:16 +00:00
if order . maker_statement not in [ None , " " ] and order . taker_statement not in [ None , " " ] :
2022-01-16 21:54:42 +00:00
order . status = Order . Status . WFR
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . WFR ) )
2022-01-16 21:54:42 +00:00
order . save ( )
return True , None
2022-01-07 11:31:33 +00:00
@classmethod
2022-01-25 14:46:02 +00:00
def payout_amount ( cls , order , user ) :
2022-02-17 19:50:10 +00:00
""" Computes buyer invoice amount. Uses order.last_satoshis,
that is the final trade amount set at Taker Bond time """
2022-01-07 11:31:33 +00:00
2022-03-03 15:40:56 +00:00
if user == order . maker :
fee_fraction = FEE * MAKER_FEE_SPLIT
elif user == order . taker :
fee_fraction = FEE * ( 1 - MAKER_FEE_SPLIT )
fee_sats = order . last_satoshis * fee_fraction
2022-03-05 20:51:16 +00:00
reward_tip = int ( config ( ' REWARD_TIP ' ) ) if user . profile . is_referred else 0
2022-01-07 11:31:33 +00:00
if cls . is_buyer ( order , user ) :
2022-03-05 20:51:16 +00:00
invoice_amount = round ( order . last_satoshis - fee_sats - reward_tip ) # Trading fee to buyer is charged here.
2022-01-07 11:31:33 +00:00
2022-02-17 19:50:10 +00:00
return True , { " invoice_amount " : invoice_amount }
2022-01-07 11:31:33 +00:00
2022-03-03 15:40:56 +00:00
@classmethod
def escrow_amount ( cls , order , user ) :
""" Computes escrow invoice amount. Uses order.last_satoshis,
that is the final trade amount set at Taker Bond time """
if user == order . maker :
fee_fraction = FEE * MAKER_FEE_SPLIT
elif user == order . taker :
fee_fraction = FEE * ( 1 - MAKER_FEE_SPLIT )
2022-03-05 20:51:16 +00:00
fee_sats = order . last_satoshis * fee_fraction
reward_tip = int ( config ( ' REWARD_TIP ' ) ) if user . profile . is_referred else 0
2022-03-03 15:40:56 +00:00
if cls . is_seller ( order , user ) :
2022-03-05 20:51:16 +00:00
escrow_amount = round ( order . last_satoshis + fee_sats + reward_tip ) # Trading fee to seller is charged here.
2022-03-03 15:40:56 +00:00
return True , { " escrow_amount " : escrow_amount }
2022-01-06 16:54:37 +00:00
@classmethod
def update_invoice ( cls , order , user , invoice ) :
2022-02-17 19:50:10 +00:00
2022-01-07 19:22:07 +00:00
# only the buyer can post a buyer invoice
2022-01-27 14:40:14 +00:00
2022-01-07 19:22:07 +00:00
if not cls . is_buyer ( order , user ) :
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " :
" Only the buyer of this order can provide a buyer invoice. "
}
2022-01-07 19:22:07 +00:00
if not order . taker_bond :
2022-02-17 19:50:10 +00:00
return False , { " bad_request " : " Wait for your order to be taken. " }
if ( not ( order . taker_bond . status == order . maker_bond . status ==
LNPayment . Status . LOCKED )
and not order . status == Order . Status . FAI ) :
return False , {
" bad_request " :
" You cannot submit a invoice while bonds are not locked. "
}
2022-03-20 23:32:25 +00:00
if order . status == Order . Status . FAI :
if order . payout . status != LNPayment . Status . EXPIRE :
return False , {
" bad_request " :
" You cannot submit an invoice only after expiration or 3 failed attempts "
}
2022-02-17 19:50:10 +00:00
num_satoshis = cls . payout_amount ( order , user ) [ 1 ] [ " invoice_amount " ]
2022-01-25 14:46:02 +00:00
payout = LNNode . validate_ln_invoice ( invoice , num_satoshis )
2022-01-12 00:02:17 +00:00
2022-02-17 19:50:10 +00:00
if not payout [ " valid " ] :
return False , payout [ " context " ]
2022-01-06 22:39:59 +00:00
2022-01-25 14:46:02 +00:00
order . payout , _ = LNPayment . objects . update_or_create (
2022-02-17 19:50:10 +00:00
concept = LNPayment . Concepts . PAYBUYER ,
type = LNPayment . Types . NORM ,
sender = User . objects . get ( username = ESCROW_USERNAME ) ,
order_paid =
order , # In case this user has other payouts, update the one related to this order.
receiver = user ,
2022-01-06 22:39:59 +00:00
# if there is a LNPayment matching these above, it updates that one with defaults below.
defaults = {
2022-02-17 19:50:10 +00:00
" invoice " : invoice ,
" status " : LNPayment . Status . VALIDI ,
" num_satoshis " : num_satoshis ,
" description " : payout [ " description " ] ,
" payment_hash " : payout [ " payment_hash " ] ,
" created_at " : payout [ " created_at " ] ,
" expires_at " : payout [ " expires_at " ] ,
} ,
)
2022-01-06 22:39:59 +00:00
2022-01-12 00:02:17 +00:00
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
2022-02-17 19:50:10 +00:00
if order . status == Order . Status . WFI :
2022-01-12 12:57:03 +00:00
order . status = Order . Status . CHA
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . CHA ) )
2022-01-06 22:39:59 +00:00
2022-01-11 01:02:06 +00:00
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
2022-01-06 22:39:59 +00:00
if order . status == Order . Status . WF2 :
2022-01-30 15:50:22 +00:00
# If the escrow does not exist, or is not locked move to WFE.
if order . trade_escrow == None :
order . status = Order . Status . WFE
# If the escrow is locked move to Chat.
elif order . trade_escrow . status == LNPayment . Status . LOCKED :
2022-01-12 12:57:03 +00:00
order . status = Order . Status . CHA
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . CHA ) )
2022-01-06 22:39:59 +00:00
else :
order . status = Order . Status . WFE
2022-02-17 19:50:10 +00:00
2022-01-24 22:53:55 +00:00
# If the order status is 'Failed Routing'. Retry payment.
2022-01-25 14:46:02 +00:00
if order . status == Order . Status . FAI :
2022-02-17 19:50:10 +00:00
if LNNode . double_check_htlc_is_settled (
order . trade_escrow . payment_hash ) :
2022-02-04 18:07:09 +00:00
order . status = Order . Status . PAY
order . payout . status = LNPayment . Status . FLIGHT
order . payout . routing_attempts = 0
order . payout . save ( )
order . save ( )
2022-01-06 22:39:59 +00:00
order . save ( )
return True , None
2022-01-06 20:33:40 +00:00
2022-01-11 20:49:53 +00:00
def add_profile_rating ( profile , rating ) :
2022-02-17 19:50:10 +00:00
""" adds a new rating to a user profile """
2022-01-11 20:49:53 +00:00
2022-01-17 23:11:41 +00:00
# TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
2022-01-20 17:30:29 +00:00
profile . total_ratings + = 1
2022-01-11 20:49:53 +00:00
latest_ratings = profile . latest_ratings
2022-01-17 23:11:41 +00:00
if latest_ratings == None :
2022-01-11 20:49:53 +00:00
profile . latest_ratings = [ rating ]
profile . avg_rating = rating
else :
2022-01-17 23:11:41 +00:00
latest_ratings = ast . literal_eval ( latest_ratings )
latest_ratings . append ( rating )
2022-01-11 20:49:53 +00:00
profile . latest_ratings = latest_ratings
2022-02-17 19:50:10 +00:00
profile . avg_rating = sum ( list ( map ( int , latest_ratings ) ) ) / len (
latest_ratings
) # Just an average, but it is a list of strings. Has to be converted to int.
2022-01-11 20:49:53 +00:00
profile . save ( )
2022-01-10 12:10:32 +00:00
def is_penalized ( user ) :
2022-02-17 19:50:10 +00:00
""" Checks if a user that is not participant of orders
has a limit on taking or making a order """
2022-01-10 12:10:32 +00:00
if user . profile . penalty_expiration :
if user . profile . penalty_expiration > timezone . now ( ) :
2022-02-17 19:50:10 +00:00
time_out = ( user . profile . penalty_expiration -
timezone . now ( ) ) . seconds
2022-01-10 12:10:32 +00:00
return True , time_out
return False , None
2022-01-06 20:33:40 +00:00
@classmethod
2022-01-07 19:22:07 +00:00
def cancel_order ( cls , order , user , state = None ) :
2022-01-07 11:31:33 +00:00
2022-01-27 14:40:14 +00:00
# Do not change order status if an is in order
# any of these status
2022-02-17 19:50:10 +00:00
do_not_cancel = [
Order . Status . DEL ,
Order . Status . UCA ,
Order . Status . EXP ,
Order . Status . TLD ,
Order . Status . DIS ,
Order . Status . CCA ,
Order . Status . PAY ,
Order . Status . SUC ,
Order . Status . FAI ,
Order . Status . MLD ,
]
2022-01-20 20:50:25 +00:00
if order . status in do_not_cancel :
2022-02-17 19:50:10 +00:00
return False , { " bad_request " : " You cannot cancel this order " }
2022-01-20 20:50:25 +00:00
2022-01-07 11:31:33 +00:00
# 1) When maker cancels before bond
2022-02-17 19:50:10 +00:00
""" The order never shows up on the book and order
status becomes " cancelled " """
2022-01-06 22:39:59 +00:00
if order . status == Order . Status . WFB and order . maker == user :
2022-02-17 02:45:18 +00:00
cls . cancel_bond ( order . maker_bond )
2022-01-06 22:39:59 +00:00
order . status = Order . Status . UCA
order . save ( )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-06 22:39:59 +00:00
2022-02-17 19:50:10 +00:00
# 2) When maker cancels after bond
""" The order dissapears from book and goes to cancelled. If strict, maker is charged the bond
2022-01-30 15:18:03 +00:00
to prevent DDOS on the LN node and order book . If not strict , maker is returned
2022-02-17 19:50:10 +00:00
the bond ( more user friendly ) . """
2022-01-11 14:36:43 +00:00
elif order . status == Order . Status . PUB and order . maker == user :
2022-02-23 16:15:48 +00:00
# Return the maker bond (Maker gets returned the bond for cancelling public order)
if cls . return_bond ( order . maker_bond ) : # strict cancellation: cls.settle_bond(order.maker_bond):
2022-01-11 14:36:43 +00:00
order . status = Order . Status . UCA
order . save ( )
2022-02-23 16:15:48 +00:00
send_message . delay ( order . id , ' public_order_cancelled ' )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-06 20:33:40 +00:00
2022-02-17 19:50:10 +00:00
# 3) When taker cancels before bond
""" The order goes back to the book as public.
LNPayment " order.taker_bond " is deleted ( ) """
2022-01-10 12:10:32 +00:00
elif order . status == Order . Status . TAK and order . taker == user :
# adds a timeout penalty
2022-01-17 18:11:44 +00:00
cls . cancel_bond ( order . taker_bond )
2022-01-14 14:19:25 +00:00
cls . kick_taker ( order )
2022-03-11 15:24:39 +00:00
# send_message.delay(order.id,'taker_canceled_b4bond') # too spammy
2022-01-12 00:02:17 +00:00
return True , None
2022-01-06 20:33:40 +00:00
2022-02-17 19:50:10 +00:00
# 4) When taker or maker cancel after bond (before escrow)
""" The order goes into cancelled status if maker cancels.
2022-01-06 22:39:59 +00:00
The order goes into the public book if taker cancels .
2022-02-17 19:50:10 +00:00
In both cases there is a small fee . """
# 4.a) When maker cancel after bond (before escrow)
""" The order into cancelled status if maker cancels. """
2022-03-07 21:46:52 +00:00
elif ( order . status in [ Order . Status . TAK , Order . Status . WF2 , Order . Status . WFE ] and order . maker == user ) :
2022-02-17 19:50:10 +00:00
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
2022-01-16 21:54:42 +00:00
valid = cls . settle_bond ( order . maker_bond )
2022-02-17 19:50:10 +00:00
cls . return_bond ( order . taker_bond ) # returns taker bond
2022-01-11 14:36:43 +00:00
if valid :
order . status = Order . Status . UCA
order . save ( )
2022-03-07 21:46:52 +00:00
# Reward taker with part of the maker bond
cls . add_slashed_rewards ( order . maker_bond , order . taker . profile )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-11 14:36:43 +00:00
2022-02-17 19:50:10 +00:00
# 4.b) When taker cancel after bond (before escrow)
""" The order into cancelled status if maker cancels. """
elif ( order . status in [ Order . Status . WF2 , Order . Status . WFE ]
and order . taker == user ) :
2022-01-11 20:49:53 +00:00
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
2022-01-16 21:54:42 +00:00
valid = cls . settle_bond ( order . taker_bond )
2022-01-11 14:36:43 +00:00
if valid :
order . taker = None
2022-01-18 15:23:57 +00:00
cls . publish_order ( order )
2022-03-11 15:55:55 +00:00
send_message . delay ( order . id , ' order_published ' )
2022-03-07 21:46:52 +00:00
# Reward maker with part of the taker bond
cls . add_slashed_rewards ( order . taker_bond , order . maker . profile )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-11 14:36:43 +00:00
2022-02-17 19:50:10 +00:00
# 5) When trade collateral has been posted (after escrow)
""" Always goes to CCA status. Collaboration is needed.
2022-01-23 19:02:25 +00:00
When a user asks for cancel , ' order.m/t/aker_asked_cancel ' goes True .
2022-01-06 22:39:59 +00:00
When the second user asks for cancel . Order is totally cancelled .
2022-02-17 19:50:10 +00:00
Must have a small cost for both parties to prevent node DDOS . """
elif order . status in [
2022-02-21 10:05:19 +00:00
Order . Status . WFI , Order . Status . CHA
2022-02-17 19:50:10 +00:00
] :
2022-01-23 19:02:25 +00:00
# if the maker had asked, and now the taker does: cancel order, return everything
if order . maker_asked_cancel and user == order . taker :
cls . collaborative_cancel ( order )
return True , None
2022-02-17 19:50:10 +00:00
2022-01-23 19:02:25 +00:00
# if the taker had asked, and now the maker does: cancel order, return everything
elif order . taker_asked_cancel and user == order . maker :
cls . collaborative_cancel ( order )
return True , None
# Otherwise just make true the asked for cancel flags
elif user == order . taker :
order . taker_asked_cancel = True
order . save ( )
return True , None
2022-02-17 19:50:10 +00:00
2022-01-23 19:02:25 +00:00
elif user == order . maker :
order . maker_asked_cancel = True
order . save ( )
return True , None
2022-01-06 22:39:59 +00:00
else :
2022-02-17 19:50:10 +00:00
return False , { " bad_request " : " You cannot cancel this order " }
2022-01-06 20:33:40 +00:00
2022-01-23 19:02:25 +00:00
@classmethod
def collaborative_cancel ( cls , order ) :
cls . return_bond ( order . maker_bond )
cls . return_bond ( order . taker_bond )
cls . return_escrow ( order )
order . status = Order . Status . CCA
order . save ( )
return
2022-01-17 23:11:41 +00:00
def publish_order ( order ) :
2022-01-18 15:23:57 +00:00
order . status = Order . Status . PUB
2022-02-17 19:50:10 +00:00
order . expires_at = order . created_at + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . PUB ) )
2022-03-22 17:49:57 +00:00
if order . has_range :
order . amount = None
2022-01-18 15:23:57 +00:00
order . save ( )
2022-03-11 15:55:55 +00:00
# send_message.delay(order.id,'order_published') # too spammy
2022-01-18 15:23:57 +00:00
return
2022-01-17 23:11:41 +00:00
2022-01-12 00:02:17 +00:00
@classmethod
def is_maker_bond_locked ( cls , order ) :
2022-01-18 13:20:19 +00:00
if order . maker_bond . status == LNPayment . Status . LOCKED :
return True
2022-01-25 14:46:02 +00:00
elif LNNode . validate_hold_invoice_locked ( order . maker_bond ) :
2022-01-17 23:11:41 +00:00
cls . publish_order ( order )
2022-03-11 15:55:55 +00:00
send_message . delay ( order . id , ' order_published ' )
2022-01-12 00:02:17 +00:00
return True
return False
2022-01-06 16:54:37 +00:00
@classmethod
2022-01-09 20:05:19 +00:00
def gen_maker_hold_invoice ( cls , order , user ) :
2022-01-06 16:54:37 +00:00
2022-01-12 00:02:17 +00:00
# Do not gen and cancel if order is older than expiry time
2022-01-06 16:54:37 +00:00
if order . expires_at < timezone . now ( ) :
cls . order_expires ( order )
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " :
" Invoice expired. You did not confirm publishing the order in time. Make a new order. "
}
2022-01-06 16:54:37 +00:00
2022-01-07 11:31:33 +00:00
# Return the previous invoice if there was one and is still unpaid
2022-01-06 16:54:37 +00:00
if order . maker_bond :
2022-01-12 00:02:17 +00:00
if cls . is_maker_bond_locked ( order ) :
2022-01-07 11:31:33 +00:00
return False , None
2022-01-12 00:02:17 +00:00
elif order . maker_bond . status == LNPayment . Status . INVGEN :
2022-02-17 19:50:10 +00:00
return True , {
" bond_invoice " : order . maker_bond . invoice ,
" bond_satoshis " : order . maker_bond . num_satoshis ,
}
2022-01-06 16:54:37 +00:00
2022-01-12 00:02:17 +00:00
# If there was no maker_bond object yet, generates one
2022-01-07 11:31:33 +00:00
order . last_satoshis = cls . satoshis_now ( order )
2022-03-18 22:09:38 +00:00
bond_satoshis = int ( order . last_satoshis * order . bond_size / 100 )
2022-01-11 14:36:43 +00:00
2022-02-04 18:07:09 +00:00
description = f " RoboSats - Publishing ' { str ( order ) } ' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. "
2022-01-06 20:33:40 +00:00
2022-01-09 20:05:19 +00:00
# Gen hold Invoice
2022-02-08 10:05:22 +00:00
try :
2022-02-17 19:50:10 +00:00
hold_payment = LNNode . gen_hold_invoice (
bond_satoshis ,
description ,
2022-03-18 21:21:13 +00:00
invoice_expiry = order . t_to_expire ( Order . Status . WFB ) ,
2022-02-17 19:50:10 +00:00
cltv_expiry_secs = BOND_EXPIRY * 3600 ,
)
2022-02-08 10:05:22 +00:00
except Exception as e :
2022-02-09 19:45:11 +00:00
print ( str ( e ) )
2022-02-17 19:50:10 +00:00
if " failed to connect to all addresses " in str ( e ) :
return False , {
" bad_request " :
" The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware. "
}
if " wallet locked " in str ( e ) :
return False , {
" bad_request " :
" This is weird, RoboSats ' lightning wallet is locked. Check in the Telegram group, maybe the staff has died. "
}
2022-01-06 16:54:37 +00:00
order . maker_bond = LNPayment . objects . create (
2022-02-17 19:50:10 +00:00
concept = LNPayment . Concepts . MAKEBOND ,
type = LNPayment . Types . HOLD ,
sender = user ,
receiver = User . objects . get ( username = ESCROW_USERNAME ) ,
invoice = hold_payment [ " invoice " ] ,
preimage = hold_payment [ " preimage " ] ,
status = LNPayment . Status . INVGEN ,
num_satoshis = bond_satoshis ,
description = description ,
payment_hash = hold_payment [ " payment_hash " ] ,
created_at = hold_payment [ " created_at " ] ,
expires_at = hold_payment [ " expires_at " ] ,
cltv_expiry = hold_payment [ " cltv_expiry " ] ,
)
2022-01-06 16:54:37 +00:00
order . save ( )
2022-02-17 19:50:10 +00:00
return True , {
" bond_invoice " : hold_payment [ " invoice " ] ,
" bond_satoshis " : bond_satoshis ,
}
2022-01-06 16:54:37 +00:00
2022-01-11 20:49:53 +00:00
@classmethod
2022-01-17 23:11:41 +00:00
def finalize_contract ( cls , order ) :
2022-02-17 19:50:10 +00:00
""" When the taker locks the taker_bond
the contract is final """
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
# (This is the last update to "last_satoshis", it becomes the escrow amount next)
order . last_satoshis = cls . satoshis_now ( order )
order . taker_bond . status = LNPayment . Status . LOCKED
order . taker_bond . save ( )
2022-03-05 12:21:01 +00:00
# With the bond confirmation the order is extended 'public_order_duration' hours
order . expires_at = timezone . now ( ) + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . WF2 ) )
2022-03-05 12:21:01 +00:00
order . status = Order . Status . WF2
order . save ( )
2022-02-17 19:50:10 +00:00
# Both users profiles are added one more contract // Unsafe can add more than once.
order . maker . profile . total_contracts + = 1
order . taker . profile . total_contracts + = 1
order . maker . profile . save ( )
order . taker . profile . save ( )
2022-03-05 12:21:01 +00:00
2022-03-05 12:19:56 +00:00
# Log a market tick
try :
MarketTick . log_a_tick ( order )
except :
pass
2022-03-11 15:24:39 +00:00
send_message . delay ( order . id , ' order_taken_confirmed ' )
2022-02-17 19:50:10 +00:00
return True
2022-01-17 23:11:41 +00:00
@classmethod
def is_taker_bond_locked ( cls , order ) :
if order . taker_bond . status == LNPayment . Status . LOCKED :
return True
2022-01-25 14:46:02 +00:00
elif LNNode . validate_hold_invoice_locked ( order . taker_bond ) :
2022-01-17 23:11:41 +00:00
cls . finalize_contract ( order )
return True
2022-01-11 20:49:53 +00:00
return False
2022-01-06 16:54:37 +00:00
@classmethod
2022-01-09 20:05:19 +00:00
def gen_taker_hold_invoice ( cls , order , user ) :
2022-01-06 16:54:37 +00:00
2022-01-12 00:02:17 +00:00
# Do not gen and kick out the taker if order is older than expiry time
if order . expires_at < timezone . now ( ) :
2022-01-19 19:37:10 +00:00
cls . order_expires ( order )
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " :
" Invoice expired. You did not confirm taking the order in time. "
}
2022-01-12 00:02:17 +00:00
# Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting.
2022-01-06 20:33:40 +00:00
if order . taker_bond :
2022-01-12 00:02:17 +00:00
if cls . is_taker_bond_locked ( order ) :
2022-01-07 11:31:33 +00:00
return False , None
2022-01-12 00:02:17 +00:00
elif order . taker_bond . status == LNPayment . Status . INVGEN :
2022-02-17 19:50:10 +00:00
return True , {
" bond_invoice " : order . taker_bond . invoice ,
" bond_satoshis " : order . taker_bond . num_satoshis ,
}
2022-01-06 16:54:37 +00:00
2022-01-12 00:02:17 +00:00
# If there was no taker_bond object yet, generates one
order . last_satoshis = cls . satoshis_now ( order )
2022-03-18 22:09:38 +00:00
bond_satoshis = int ( order . last_satoshis * order . bond_size / 100 )
2022-02-17 19:50:10 +00:00
pos_text = " Buying " if cls . is_buyer ( order , user ) else " Selling "
description = (
f " RoboSats - Taking ' Order { order . id } ' { pos_text } BTC for { str ( float ( order . amount ) ) + Currency . currency_dict [ str ( order . currency . currency ) ] } "
+
" - Taker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. "
)
2022-01-06 20:33:40 +00:00
2022-01-09 20:05:19 +00:00
# Gen hold Invoice
2022-02-08 10:05:22 +00:00
try :
2022-02-17 19:50:10 +00:00
hold_payment = LNNode . gen_hold_invoice (
bond_satoshis ,
description ,
2022-03-18 21:21:13 +00:00
invoice_expiry = order . t_to_expire ( Order . Status . TAK ) ,
2022-02-17 19:50:10 +00:00
cltv_expiry_secs = BOND_EXPIRY * 3600 ,
)
2022-02-08 10:05:22 +00:00
except Exception as e :
2022-02-17 19:50:10 +00:00
if " status = StatusCode.UNAVAILABLE " in str ( e ) :
return False , {
" bad_request " :
" The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware. "
}
2022-01-06 20:33:40 +00:00
order . taker_bond = LNPayment . objects . create (
2022-02-17 19:50:10 +00:00
concept = LNPayment . Concepts . TAKEBOND ,
type = LNPayment . Types . HOLD ,
sender = user ,
receiver = User . objects . get ( username = ESCROW_USERNAME ) ,
invoice = hold_payment [ " invoice " ] ,
preimage = hold_payment [ " preimage " ] ,
status = LNPayment . Status . INVGEN ,
num_satoshis = bond_satoshis ,
description = description ,
payment_hash = hold_payment [ " payment_hash " ] ,
created_at = hold_payment [ " created_at " ] ,
expires_at = hold_payment [ " expires_at " ] ,
cltv_expiry = hold_payment [ " cltv_expiry " ] ,
)
order . expires_at = timezone . now ( ) + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . TAK ) )
2022-01-06 16:54:37 +00:00
order . save ( )
2022-02-17 19:50:10 +00:00
return True , {
" bond_invoice " : hold_payment [ " invoice " ] ,
" bond_satoshis " : bond_satoshis ,
}
2022-01-07 11:31:33 +00:00
2022-01-18 13:20:19 +00:00
def trade_escrow_received ( order ) :
2022-02-17 19:50:10 +00:00
""" Moves the order forward """
2022-01-18 13:20:19 +00:00
# If status is 'Waiting for both' move to Waiting for invoice
if order . status == Order . Status . WF2 :
order . status = Order . Status . WFI
# If status is 'Waiting for invoice' move to Chat
elif order . status == Order . Status . WFE :
order . status = Order . Status . CHA
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-03-18 21:21:13 +00:00
seconds = order . t_to_expire ( Order . Status . CHA ) )
2022-01-18 13:20:19 +00:00
order . save ( )
2022-01-12 00:02:17 +00:00
@classmethod
def is_trade_escrow_locked ( cls , order ) :
2022-01-18 13:20:19 +00:00
if order . trade_escrow . status == LNPayment . Status . LOCKED :
return True
2022-01-25 14:46:02 +00:00
elif LNNode . validate_hold_invoice_locked ( order . trade_escrow ) :
2022-01-18 13:20:19 +00:00
cls . trade_escrow_received ( order )
2022-01-12 00:02:17 +00:00
return True
return False
2022-01-07 11:31:33 +00:00
@classmethod
2022-01-09 20:05:19 +00:00
def gen_escrow_hold_invoice ( cls , order , user ) :
2022-01-12 00:02:17 +00:00
# Do not generate if escrow deposit time has expired
if order . expires_at < timezone . now ( ) :
2022-01-19 19:37:10 +00:00
cls . order_expires ( order )
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " :
" Invoice expired. You did not send the escrow in time. "
}
2022-01-12 00:02:17 +00:00
# Do not gen if an escrow invoice exist. Do not return if it is already locked. Return the old one if still waiting.
2022-01-07 11:31:33 +00:00
if order . trade_escrow :
# Check if status is INVGEN and still not expired
2022-01-12 00:02:17 +00:00
if cls . is_trade_escrow_locked ( order ) :
return False , None
elif order . trade_escrow . status == LNPayment . Status . INVGEN :
2022-02-17 19:50:10 +00:00
return True , {
" escrow_invoice " : order . trade_escrow . invoice ,
" escrow_satoshis " : order . trade_escrow . num_satoshis ,
}
2022-01-07 11:31:33 +00:00
2022-01-18 13:20:19 +00:00
# If there was no taker_bond object yet, generate one
2022-03-03 15:40:56 +00:00
escrow_satoshis = cls . escrow_amount ( order , user ) [ 1 ] [ " escrow_amount " ] # Amount was fixed when taker bond was locked, fee applied here
2022-02-04 18:07:09 +00:00
description = f " RoboSats - Escrow amount for ' { str ( order ) } ' - It WILL FREEZE IN YOUR WALLET. It will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment. "
2022-01-07 11:31:33 +00:00
2022-01-09 20:05:19 +00:00
# Gen hold Invoice
2022-02-08 10:05:22 +00:00
try :
2022-02-17 19:50:10 +00:00
hold_payment = LNNode . gen_hold_invoice (
escrow_satoshis ,
description ,
2022-03-18 21:21:13 +00:00
invoice_expiry = order . t_to_expire ( Order . Status . WF2 ) ,
2022-02-17 19:50:10 +00:00
cltv_expiry_secs = ESCROW_EXPIRY * 3600 ,
)
2022-02-08 10:05:22 +00:00
except Exception as e :
2022-02-17 19:50:10 +00:00
if " status = StatusCode.UNAVAILABLE " in str ( e ) :
return False , {
" bad_request " :
" The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware. "
}
2022-02-08 10:05:22 +00:00
2022-01-08 17:19:30 +00:00
order . trade_escrow = LNPayment . objects . create (
2022-02-17 19:50:10 +00:00
concept = LNPayment . Concepts . TRESCROW ,
type = LNPayment . Types . HOLD ,
sender = user ,
receiver = User . objects . get ( username = ESCROW_USERNAME ) ,
invoice = hold_payment [ " invoice " ] ,
preimage = hold_payment [ " preimage " ] ,
status = LNPayment . Status . INVGEN ,
num_satoshis = escrow_satoshis ,
description = description ,
payment_hash = hold_payment [ " payment_hash " ] ,
created_at = hold_payment [ " created_at " ] ,
expires_at = hold_payment [ " expires_at " ] ,
cltv_expiry = hold_payment [ " cltv_expiry " ] ,
)
2022-01-07 11:31:33 +00:00
order . save ( )
2022-02-17 19:50:10 +00:00
return True , {
" escrow_invoice " : hold_payment [ " invoice " ] ,
" escrow_satoshis " : escrow_satoshis ,
}
2022-01-07 18:22:52 +00:00
def settle_escrow ( order ) :
2022-02-17 19:50:10 +00:00
""" Settles the trade escrow hold invoice """
2022-01-11 14:36:43 +00:00
# TODO ERROR HANDLING
2022-01-12 00:02:17 +00:00
if LNNode . settle_hold_invoice ( order . trade_escrow . preimage ) :
2022-01-11 14:36:43 +00:00
order . trade_escrow . status = LNPayment . Status . SETLED
2022-01-12 00:02:17 +00:00
order . trade_escrow . save ( )
return True
2022-01-11 14:36:43 +00:00
2022-01-16 21:54:42 +00:00
def settle_bond ( bond ) :
2022-02-17 19:50:10 +00:00
""" Settles the bond hold invoice """
2022-01-11 14:36:43 +00:00
# TODO ERROR HANDLING
2022-01-16 21:54:42 +00:00
if LNNode . settle_hold_invoice ( bond . preimage ) :
bond . status = LNPayment . Status . SETLED
bond . save ( )
2022-01-11 20:49:53 +00:00
return True
2022-01-11 14:36:43 +00:00
2022-01-16 21:54:42 +00:00
def return_escrow ( order ) :
2022-02-17 19:50:10 +00:00
""" returns the trade escrow """
2022-01-16 21:54:42 +00:00
if LNNode . cancel_return_hold_invoice ( order . trade_escrow . payment_hash ) :
order . trade_escrow . status = LNPayment . Status . RETNED
2022-01-20 17:30:29 +00:00
order . trade_escrow . save ( )
2022-01-12 00:02:17 +00:00
return True
2022-01-16 21:54:42 +00:00
2022-01-17 23:11:41 +00:00
def cancel_escrow ( order ) :
2022-02-17 19:50:10 +00:00
""" returns the trade escrow """
2022-01-17 23:11:41 +00:00
# Same as return escrow, but used when the invoice was never LOCKED
if LNNode . cancel_return_hold_invoice ( order . trade_escrow . payment_hash ) :
order . trade_escrow . status = LNPayment . Status . CANCEL
2022-01-20 17:30:29 +00:00
order . trade_escrow . save ( )
2022-01-17 23:11:41 +00:00
return True
2022-01-12 12:57:03 +00:00
def return_bond ( bond ) :
2022-02-17 19:50:10 +00:00
""" returns a bond """
2022-01-17 18:11:44 +00:00
if bond == None :
return
try :
LNNode . cancel_return_hold_invoice ( bond . payment_hash )
2022-01-12 12:57:03 +00:00
bond . status = LNPayment . Status . RETNED
2022-01-20 17:30:29 +00:00
bond . save ( )
2022-01-12 12:57:03 +00:00
return True
2022-01-17 18:11:44 +00:00
except Exception as e :
2022-02-17 19:50:10 +00:00
if " invoice already settled " in str ( e ) :
2022-01-17 18:11:44 +00:00
bond . status = LNPayment . Status . SETLED
2022-01-20 17:30:29 +00:00
bond . save ( )
2022-01-17 18:11:44 +00:00
return True
2022-01-17 23:11:41 +00:00
else :
raise e
2022-01-17 18:11:44 +00:00
def cancel_bond ( bond ) :
2022-02-17 19:50:10 +00:00
""" cancel a bond """
2022-01-17 23:11:41 +00:00
# Same as return bond, but used when the invoice was never LOCKED
2022-01-17 18:11:44 +00:00
if bond == None :
return True
try :
LNNode . cancel_return_hold_invoice ( bond . payment_hash )
bond . status = LNPayment . Status . CANCEL
2022-01-20 17:30:29 +00:00
bond . save ( )
2022-01-17 18:11:44 +00:00
return True
except Exception as e :
2022-02-17 19:50:10 +00:00
if " invoice already settled " in str ( e ) :
2022-01-17 18:11:44 +00:00
bond . status = LNPayment . Status . SETLED
2022-01-20 17:30:29 +00:00
bond . save ( )
2022-01-17 18:11:44 +00:00
return True
2022-01-17 23:11:41 +00:00
else :
raise e
2022-01-07 18:22:52 +00:00
@classmethod
def confirm_fiat ( cls , order , user ) :
2022-02-17 19:50:10 +00:00
""" If Order is in the CHAT states:
2022-01-20 20:50:25 +00:00
If user is buyer : fiat_sent goes to true .
2022-02-17 19:50:10 +00:00
If User is seller and fiat_sent is true : settle the escrow and pay buyer invoice ! """
if ( order . status == Order . Status . CHA
or order . status == Order . Status . FSE
) : # TODO Alternatively, if all collateral is locked? test out
2022-01-07 18:22:52 +00:00
# If buyer, settle escrow and mark fiat sent
if cls . is_buyer ( order , user ) :
2022-01-19 19:37:10 +00:00
order . status = Order . Status . FSE
order . is_fiat_sent = True
2022-01-07 18:22:52 +00:00
2022-01-20 20:50:25 +00:00
# If seller and fiat was sent, SETTLE ESCROW AND PAY BUYER INVOICE
2022-01-07 18:22:52 +00:00
elif cls . is_seller ( order , user ) :
if not order . is_fiat_sent :
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " :
" You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer. "
}
2022-01-19 19:37:10 +00:00
2022-02-17 19:50:10 +00:00
# Make sure the trade escrow is at least as big as the buyer invoice
if order . trade_escrow . num_satoshis < = order . payout . num_satoshis :
return False , {
" bad_request " :
" Woah, something broke badly. Report in the public channels, or open a Github Issue. "
}
if cls . settle_escrow (
order
) : ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
2022-01-19 19:37:10 +00:00
order . trade_escrow . status = LNPayment . Status . SETLED
2022-02-17 19:50:10 +00:00
2022-01-07 18:22:52 +00:00
# Double check the escrow is settled.
2022-02-17 19:50:10 +00:00
if LNNode . double_check_htlc_is_settled (
order . trade_escrow . payment_hash ) :
2022-01-29 19:51:26 +00:00
# RETURN THE BONDS // Probably best also do it even if payment failed
cls . return_bond ( order . taker_bond )
cls . return_bond ( order . maker_bond )
2022-02-04 01:37:24 +00:00
##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
2022-03-05 18:43:15 +00:00
##### Background process "follow_invoices" will try to pay this invoice until success
2022-02-04 01:37:24 +00:00
order . status = Order . Status . PAY
order . payout . status = LNPayment . Status . FLIGHT
order . payout . save ( )
order . save ( )
2022-02-23 16:15:48 +00:00
send_message . delay ( order . id , ' trade_successful ' )
2022-03-05 18:43:15 +00:00
# Add referral rewards (safe)
try :
2022-03-07 21:46:52 +00:00
cls . add_rewards ( order )
2022-03-05 18:43:15 +00:00
except :
pass
2022-02-04 01:37:24 +00:00
return True , None
2022-02-22 17:50:01 +00:00
2022-01-07 18:22:52 +00:00
else :
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " :
" You cannot confirm the fiat payment at this stage "
}
2022-01-07 18:22:52 +00:00
order . save ( )
2022-01-12 00:02:17 +00:00
return True , None
@classmethod
def rate_counterparty ( cls , order , user , rating ) :
2022-02-17 19:50:10 +00:00
rating_allowed_status = [
Order . Status . PAY ,
Order . Status . SUC ,
Order . Status . FAI ,
Order . Status . MLD ,
Order . Status . TLD ,
]
2022-01-12 00:02:17 +00:00
# If the trade is finished
2022-01-27 14:40:14 +00:00
if order . status in rating_allowed_status :
2022-01-12 00:02:17 +00:00
# if maker, rates taker
2022-01-17 23:11:41 +00:00
if order . maker == user and order . maker_rated == False :
2022-01-12 00:02:17 +00:00
cls . add_profile_rating ( order . taker . profile , rating )
2022-01-17 23:11:41 +00:00
order . maker_rated = True
order . save ( )
2022-01-12 00:02:17 +00:00
# if taker, rates maker
2022-01-17 23:11:41 +00:00
if order . taker == user and order . taker_rated == False :
2022-01-12 00:02:17 +00:00
cls . add_profile_rating ( order . maker . profile , rating )
2022-01-17 23:11:41 +00:00
order . taker_rated = True
order . save ( )
2022-01-12 00:02:17 +00:00
else :
2022-02-17 19:50:10 +00:00
return False , {
" bad_request " : " You cannot rate your counterparty yet. "
}
2022-01-12 00:02:17 +00:00
2022-02-04 18:07:09 +00:00
return True , None
@classmethod
def rate_platform ( cls , user , rating ) :
user . profile . platform_rating = rating
user . profile . save ( )
2022-02-13 16:43:49 +00:00
return True , None
2022-02-21 23:41:36 +00:00
2022-03-05 18:43:15 +00:00
@classmethod
def add_rewards ( cls , order ) :
'''
2022-03-05 20:51:16 +00:00
This function is called when a trade is finished .
If participants of the order were referred , the reward is given to the referees .
2022-03-05 18:43:15 +00:00
'''
2022-03-05 20:51:16 +00:00
2022-03-05 18:43:15 +00:00
if order . maker . profile . is_referred :
2022-03-05 20:51:16 +00:00
profile = order . maker . profile . referred_by
profile . pending_rewards + = int ( config ( ' REWARD_TIP ' ) )
profile . save ( )
2022-03-05 18:43:15 +00:00
if order . taker . profile . is_referred :
2022-03-05 20:51:16 +00:00
profile = order . taker . profile . referred_by
profile . pending_rewards + = int ( config ( ' REWARD_TIP ' ) )
profile . save ( )
2022-03-05 18:43:15 +00:00
return
2022-03-07 21:46:52 +00:00
@classmethod
def add_slashed_rewards ( cls , bond , profile ) :
'''
When a bond is slashed due to overtime , rewards the user that was waiting .
If participants of the order were referred , the reward is given to the referees .
'''
reward_fraction = float ( config ( ' SLASHED_BOND_REWARD_SPLIT ' ) )
reward = int ( bond . num_satoshis * reward_fraction )
profile . earned_rewards + = reward
profile . save ( )
return
2022-03-06 16:08:28 +00:00
@classmethod
def withdraw_rewards ( cls , user , invoice ) :
# only a user with positive withdraw balance can use this
if user . profile . earned_rewards < 1 :
return False , { " bad_invoice " : " You have not earned rewards " }
num_satoshis = user . profile . earned_rewards
2022-03-09 11:35:50 +00:00
2022-03-06 16:08:28 +00:00
reward_payout = LNNode . validate_ln_invoice ( invoice , num_satoshis )
if not reward_payout [ " valid " ] :
return False , reward_payout [ " context " ]
2022-03-09 11:35:50 +00:00
try :
lnpayment = LNPayment . objects . create (
concept = LNPayment . Concepts . WITHREWA ,
type = LNPayment . Types . NORM ,
sender = User . objects . get ( username = ESCROW_USERNAME ) ,
status = LNPayment . Status . VALIDI ,
receiver = user ,
invoice = invoice ,
num_satoshis = num_satoshis ,
description = reward_payout [ " description " ] ,
payment_hash = reward_payout [ " payment_hash " ] ,
created_at = reward_payout [ " created_at " ] ,
expires_at = reward_payout [ " expires_at " ] ,
)
# Might fail if payment_hash already exists in DB
except :
return False , { " bad_invoice " : " Give me a new invoice " }
2022-03-06 16:08:28 +00:00
2022-03-09 11:35:50 +00:00
user . profile . earned_rewards = 0
user . profile . save ( )
# Pays the invoice.
paid , failure_reason = LNNode . pay_invoice ( lnpayment )
if paid :
2022-03-06 16:08:28 +00:00
user . profile . earned_rewards = 0
user . profile . claimed_rewards + = num_satoshis
user . profile . save ( )
2022-03-09 11:35:50 +00:00
return True , None
2022-03-06 16:08:28 +00:00
2022-03-09 11:35:50 +00:00
# If fails, adds the rewards again.
else :
user . profile . earned_rewards = num_satoshis
user . profile . save ( )
context = { }
context [ ' bad_invoice ' ] = failure_reason
return False , context
2022-03-06 16:08:28 +00:00