2022-01-06 16:54:37 +00:00
from datetime import timedelta
from django . utils import timezone
from . lightning import LNNode
2022-01-07 22:46:30 +00:00
from . models import Order , LNPayment , MarketTick , User
2022-01-06 16:54:37 +00:00
from decouple import config
2022-01-07 22:46:30 +00:00
from . utils import get_exchange_rate
2022-01-06 16:54:37 +00:00
FEE = float ( config ( ' FEE ' ) )
BOND_SIZE = float ( config ( ' BOND_SIZE ' ) )
MARKET_PRICE_API = config ( ' MARKET_PRICE_API ' )
ESCROW_USERNAME = config ( ' ESCROW_USERNAME ' )
2022-01-10 12:10:32 +00:00
PENALTY_TIMEOUT = int ( config ( ' PENALTY_TIMEOUT ' ) )
2022-01-06 16:54:37 +00:00
2022-01-06 21:36:22 +00:00
MIN_TRADE = int ( config ( ' MIN_TRADE ' ) )
MAX_TRADE = int ( config ( ' MAX_TRADE ' ) )
2022-01-06 20:33:40 +00:00
EXP_MAKER_BOND_INVOICE = int ( config ( ' EXP_MAKER_BOND_INVOICE ' ) )
EXP_TAKER_BOND_INVOICE = int ( config ( ' EXP_TAKER_BOND_INVOICE ' ) )
EXP_TRADE_ESCR_INVOICE = int ( config ( ' EXP_TRADE_ESCR_INVOICE ' ) )
BOND_EXPIRY = int ( config ( ' BOND_EXPIRY ' ) )
ESCROW_EXPIRY = int ( config ( ' ESCROW_EXPIRY ' ) )
2022-01-06 16:54:37 +00:00
2022-01-10 12:10:32 +00:00
2022-01-06 16:54:37 +00:00
class Logics ( ) :
def validate_already_maker_or_taker ( user ) :
''' Checks if the user is already partipant of an order '''
queryset = Order . objects . filter ( maker = user )
if queryset . exists ( ) :
2022-01-07 11:31:33 +00:00
return False , { ' bad_request ' : ' You are already maker of an order ' }
2022-01-06 16:54:37 +00:00
queryset = Order . objects . filter ( taker = user )
if queryset . exists ( ) :
2022-01-07 11:31:33 +00:00
return False , { ' bad_request ' : ' You are already taker of an order ' }
2022-01-06 16:54:37 +00:00
return True , None
2022-01-06 21:36:22 +00:00
def validate_order_size ( order ) :
''' Checks if order is withing limits at t0 '''
if order . t0_satoshis > MAX_TRADE :
2022-01-10 01:12:58 +00:00
return False , { ' bad_request ' : ' Your order is too big. It is worth ' + ' {:,} ' . format ( order . t0_satoshis ) + ' Sats now. But limit is ' + ' {:,} ' . format ( MAX_TRADE ) + ' Sats ' }
2022-01-06 21:36:22 +00:00
if order . t0_satoshis < MIN_TRADE :
2022-01-10 01:12:58 +00:00
return False , { ' bad_request ' : ' Your order is too small. It is worth ' + ' {:,} ' . format ( order . t0_satoshis ) + ' Sats now. But limit is ' + ' {:,} ' . format ( MIN_TRADE ) + ' Sats ' }
2022-01-06 21:36:22 +00:00
return True , None
2022-01-10 12:10:32 +00:00
@classmethod
def take ( cls , order , user ) :
is_penalized , time_out = cls . is_penalized ( user )
if is_penalized :
return False , { ' bad_request ' , f ' You need to wait { time_out } seconds to take an order ' }
else :
order . taker = user
order . status = Order . Status . TAK
order . save ( )
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
return ( is_maker and order . type == Order . Types . BUY ) or ( is_taker and order . type == Order . Types . SELL )
def is_seller ( order , user ) :
is_maker = order . maker == user
is_taker = order . taker == user
return ( is_maker and order . type == Order . Types . SELL ) or ( is_taker and order . type == Order . Types . BUY )
def satoshis_now ( order ) :
''' checks trade amount in sats '''
if order . is_explicit :
satoshis_now = order . satoshis
else :
2022-01-09 01:18:11 +00:00
exchange_rate = get_exchange_rate ( Order . currency_dict [ str ( order . currency ) ] )
2022-01-07 22:46:30 +00:00
premium_rate = exchange_rate * ( 1 + float ( order . premium ) / 100 )
satoshis_now = ( float ( order . amount ) / premium_rate ) * 100 * 1000 * 1000
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 ) :
''' computes order premium live '''
exchange_rate = get_exchange_rate ( Order . currency_dict [ str ( order . currency ) ] )
if not order . is_explicit :
premium = order . premium
price = exchange_rate
else :
exchange_rate = get_exchange_rate ( Order . currency_dict [ str ( order . currency ) ] )
order_rate = float ( order . amount ) / ( float ( order . satoshis ) / 100000000 )
premium = order_rate / exchange_rate - 1
price = order_rate
premium = int ( premium * 100 ) # 2 decimals left
return price , premium
2022-01-06 16:54:37 +00:00
def order_expires ( order ) :
order . status = Order . Status . EXP
order . maker = None
order . taker = None
order . save ( )
2022-01-07 11:31:33 +00:00
@classmethod
def buyer_invoice_amount ( cls , order , user ) :
''' Computes buyer invoice amount. Uses order.last_satoshis,
that is the final trade amount set at Taker Bond time '''
if cls . is_buyer ( order , user ) :
invoice_amount = int ( order . last_satoshis * ( 1 - FEE ) ) # Trading FEE is charged here.
return True , { ' invoice_amount ' : invoice_amount }
2022-01-06 16:54:37 +00:00
@classmethod
def update_invoice ( cls , order , user , invoice ) :
2022-01-07 19:22:07 +00:00
# only the buyer can post a buyer invoice
if not cls . is_buyer ( order , user ) :
return False , { ' bad_request ' : ' Only the buyer of this order can provide a buyer invoice. ' }
if not order . taker_bond :
return False , { ' bad_request ' : ' Wait for your order to be taken. ' }
if not ( order . taker_bond . status == order . maker_bond . status == LNPayment . Status . LOCKED ) :
return False , { ' bad_request ' : ' You cannot a invoice while bonds are not posted. ' }
num_satoshis = cls . buyer_invoice_amount ( order , user ) [ 1 ] [ ' invoice_amount ' ]
valid , context , description , payment_hash , expires_at = LNNode . validate_ln_invoice ( invoice , num_satoshis )
if not valid :
return False , context
2022-01-06 22:39:59 +00:00
order . buyer_invoice , _ = LNPayment . objects . update_or_create (
concept = LNPayment . Concepts . PAYBUYER ,
type = LNPayment . Types . NORM ,
sender = User . objects . get ( username = ESCROW_USERNAME ) ,
receiver = user ,
# if there is a LNPayment matching these above, it updates that one with defaults below.
defaults = {
' invoice ' : invoice ,
' status ' : LNPayment . Status . VALIDI ,
' num_satoshis ' : num_satoshis ,
' description ' : description ,
' payment_hash ' : payment_hash ,
' expires_at ' : expires_at }
)
# If the order status is 'Waiting for invoice'. Move forward to 'waiting for invoice'
if order . status == Order . Status . WFE : order . status = Order . Status . CHA
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' or to 'chat'
if order . status == Order . Status . WF2 :
print ( order . trade_escrow )
if order . trade_escrow :
if order . trade_escrow . status == LNPayment . Status . LOCKED :
order . status = Order . Status . CHA
else :
order . status = Order . Status . WFE
order . save ( )
return True , None
2022-01-06 20:33:40 +00:00
2022-01-07 11:31:33 +00:00
@classmethod
def rate_counterparty ( cls , order , user , rating ) :
2022-01-07 18:22:52 +00:00
# If the trade is finished
if order . status > Order . Status . PAY :
# if maker, rates taker
if order . maker == user :
order . taker . profile . total_ratings = order . taker . profile . total_ratings + 1
last_ratings = list ( order . taker . profile . last_ratings ) . append ( rating )
order . taker . profile . total_ratings = sum ( last_ratings ) / len ( last_ratings )
# if taker, rates maker
if order . taker == user :
order . maker . profile . total_ratings = order . maker . profile . total_ratings + 1
last_ratings = list ( order . maker . profile . last_ratings ) . append ( rating )
order . maker . profile . total_ratings = sum ( last_ratings ) / len ( last_ratings )
else :
return False , { ' bad_request ' : ' You cannot rate your counterparty yet. ' }
2022-01-07 11:31:33 +00:00
order . save ( )
return True , None
2022-01-06 20:33:40 +00:00
2022-01-10 12:10:32 +00:00
def is_penalized ( user ) :
''' Checks if a user that is not participant of orders
has a limit on taking or making a order '''
if user . profile . penalty_expiration :
if user . profile . penalty_expiration > timezone . now ( ) :
time_out = ( user . profile . penalty_expiration - timezone . now ( ) ) . seconds
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
# 1) When maker cancels before bond
2022-01-06 22:39:59 +00:00
''' The order never shows up on the book and order
status becomes " cancelled " . That ' s it. ' ' '
if order . status == Order . Status . WFB and order . maker == user :
order . maker = None
order . status = Order . Status . UCA
order . save ( )
return True , None
# 2) When maker cancels after bond
''' The order dissapears from book and goes to cancelled.
2022-01-10 12:10:32 +00:00
Maker is charged the bond to prevent DDOS
on the LN node and order book . TODO Only charge a small part
of the bond ( requires maker submitting an invoice ) '''
2022-01-06 20:33:40 +00:00
2022-01-06 22:39:59 +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
user . profile . penalty_expiration = timezone . now ( ) + timedelta ( seconds = PENALTY_TIMEOUT )
user . save ( )
order . taker = None
order . status = Order . Status . PUB
order . save ( )
return True , None
2022-01-06 20:33:40 +00:00
2022-01-07 11:31:33 +00:00
# 4) When taker or maker cancel after bond (before escrow)
2022-01-06 22:39:59 +00:00
''' The order goes into cancelled status if maker cancels.
The order goes into the public book if taker cancels .
In both cases there is a small fee . '''
2022-01-07 11:31:33 +00:00
# 5) When trade collateral has been posted (after escrow)
2022-01-06 22:39:59 +00:00
''' Always goes to cancelled status. Collaboration is needed.
When a user asks for cancel , ' order.is_pending_cancel ' goes True .
When the second user asks for cancel . Order is totally cancelled .
Has a small cost for both parties to prevent node DDOS . '''
else :
return False , { ' bad_request ' : ' You cannot cancel this order ' }
2022-01-06 20:33:40 +00:00
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-07 11:31:33 +00:00
# Do not gen and cancel if order is more than 5 minutes old
2022-01-06 16:54:37 +00:00
if order . expires_at < timezone . now ( ) :
cls . order_expires ( order )
2022-01-07 11:31:33 +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-07 11:31:33 +00:00
if order . maker_bond . status == LNPayment . Status . INVGEN :
return True , { ' bond_invoice ' : order . maker_bond . invoice , ' bond_satoshis ' : order . maker_bond . num_satoshis }
else :
return False , None
2022-01-06 16:54:37 +00:00
2022-01-07 11:31:33 +00:00
order . last_satoshis = cls . satoshis_now ( order )
bond_satoshis = int ( order . last_satoshis * BOND_SIZE )
2022-01-06 23:33:55 +00:00
description = f ' RoboSats - Publishing { str ( order ) } - This bond will return to you if you do not cheat. '
2022-01-06 20:33:40 +00:00
2022-01-09 20:05:19 +00:00
# Gen hold Invoice
invoice , payment_hash , expires_at = LNNode . gen_hold_invoice ( bond_satoshis , description , BOND_EXPIRY * 3600 )
2022-01-06 16:54:37 +00:00
order . maker_bond = LNPayment . objects . create (
concept = LNPayment . Concepts . MAKEBOND ,
2022-01-09 20:05:19 +00:00
type = LNPayment . Types . hold ,
2022-01-06 16:54:37 +00:00
sender = user ,
receiver = User . objects . get ( username = ESCROW_USERNAME ) ,
invoice = invoice ,
status = LNPayment . Status . INVGEN ,
num_satoshis = bond_satoshis ,
description = description ,
payment_hash = payment_hash ,
2022-01-06 20:33:40 +00:00
expires_at = expires_at )
2022-01-06 16:54:37 +00:00
order . save ( )
2022-01-07 11:31:33 +00:00
return True , { ' bond_invoice ' : invoice , ' bond_satoshis ' : bond_satoshis }
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-07 11:31:33 +00:00
# Do not gen and cancel if a taker invoice is there and older than X minutes and unpaid still
2022-01-06 20:33:40 +00:00
if order . taker_bond :
2022-01-07 11:31:33 +00:00
# Check if status is INVGEN and still not expired
if order . taker_bond . status == LNPayment . Status . INVGEN :
if order . taker_bond . created_at > ( timezone . now ( ) + timedelta ( minutes = EXP_TAKER_BOND_INVOICE ) ) :
cls . cancel_order ( order , user , 3 ) # State 3, cancel order before taker bond
return False , { ' bad_request ' : ' Invoice expired. You did not confirm taking the order in time. ' }
# Return the previous invoice there was with INVGEN status
else :
return True , { ' bond_invoice ' : order . taker_bond . invoice , ' bond_satoshis ' : order . taker_bond . num_satoshis }
# Invoice exists, but was already locked or settled
2022-01-06 21:36:22 +00:00
else :
2022-01-07 11:31:33 +00:00
return False , None
2022-01-06 16:54:37 +00:00
2022-01-07 11:31:33 +00:00
order . last_satoshis = cls . satoshis_now ( order ) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE
bond_satoshis = int ( order . last_satoshis * BOND_SIZE )
2022-01-06 23:33:55 +00:00
description = f ' RoboSats - Taking { str ( order ) } - This bond will return to you if you do not cheat. '
2022-01-06 20:33:40 +00:00
2022-01-09 20:05:19 +00:00
# Gen hold Invoice
invoice , payment_hash , expires_at = LNNode . gen_hold_invoice ( bond_satoshis , description , BOND_EXPIRY * 3600 )
2022-01-06 16:54:37 +00:00
2022-01-06 20:33:40 +00:00
order . taker_bond = LNPayment . objects . create (
concept = LNPayment . Concepts . TAKEBOND ,
2022-01-09 20:05:19 +00:00
type = LNPayment . Types . hold ,
2022-01-06 16:54:37 +00:00
sender = user ,
receiver = User . objects . get ( username = ESCROW_USERNAME ) ,
invoice = invoice ,
status = LNPayment . Status . INVGEN ,
num_satoshis = bond_satoshis ,
description = description ,
payment_hash = payment_hash ,
2022-01-06 20:33:40 +00:00
expires_at = expires_at )
2022-01-06 16:54:37 +00:00
2022-01-08 17:19:30 +00:00
# Extend expiry time to allow for escrow deposit
## Not here, on func for confirming taker collar. order.expires_at = timezone.now() + timedelta(minutes=EXP_TRADE_ESCR_INVOICE)
2022-01-06 16:54:37 +00:00
order . save ( )
2022-01-07 11:31:33 +00:00
return True , { ' bond_invoice ' : invoice , ' bond_satoshis ' : bond_satoshis }
@classmethod
2022-01-09 20:05:19 +00:00
def gen_escrow_hold_invoice ( cls , order , user ) :
2022-01-07 11:31:33 +00:00
# Do not generate and cancel if an invoice is there and older than X minutes and unpaid still
if order . trade_escrow :
# Check if status is INVGEN and still not expired
2022-01-08 17:19:30 +00:00
if order . trade_escrow . status == LNPayment . Status . INVGEN :
if order . trade_escrow . created_at > ( timezone . now ( ) + timedelta ( minutes = EXP_TRADE_ESCR_INVOICE ) ) : # Expired
2022-01-07 11:31:33 +00:00
cls . cancel_order ( order , user , 4 ) # State 4, cancel order before trade escrow locked
return False , { ' bad_request ' : ' Invoice expired. You did not lock the trade escrow in time. ' }
# Return the previous invoice there was with INVGEN status
else :
2022-01-08 17:19:30 +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
# Invoice exists, but was already locked or settled
else :
return False , None # Does not return any context of a healthy locked escrow
escrow_satoshis = order . last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis)
description = f ' RoboSats - Escrow amount for { str ( order ) } - This escrow will be released to the buyer once you confirm you received the fiat. '
2022-01-09 20:05:19 +00:00
# Gen hold Invoice
invoice , payment_hash , expires_at = LNNode . gen_hold_invoice ( escrow_satoshis , description , ESCROW_EXPIRY * 3600 )
2022-01-07 11:31:33 +00:00
2022-01-08 17:19:30 +00:00
order . trade_escrow = LNPayment . objects . create (
2022-01-07 11:31:33 +00:00
concept = LNPayment . Concepts . TRESCROW ,
2022-01-09 20:05:19 +00:00
type = LNPayment . Types . hold ,
2022-01-07 11:31:33 +00:00
sender = user ,
receiver = User . objects . get ( username = ESCROW_USERNAME ) ,
invoice = invoice ,
status = LNPayment . Status . INVGEN ,
num_satoshis = escrow_satoshis ,
description = description ,
payment_hash = payment_hash ,
expires_at = expires_at )
order . save ( )
2022-01-07 18:22:52 +00:00
return True , { ' escrow_invoice ' : invoice , ' escrow_satoshis ' : escrow_satoshis }
2022-01-07 22:46:30 +00:00
2022-01-07 18:22:52 +00:00
def settle_escrow ( order ) :
''' Settles the trade escrow HTLC '''
# TODO ERROR HANDLING
2022-01-09 20:05:19 +00:00
valid = LNNode . settle_hold_htlcs ( order . trade_escrow . payment_hash )
2022-01-07 18:22:52 +00:00
return valid
def pay_buyer_invoice ( order ) :
''' Settles the trade escrow HTLC '''
# TODO ERROR HANDLING
valid = LNNode . pay_invoice ( order . buyer_invoice . payment_hash )
return valid
@classmethod
def confirm_fiat ( cls , order , user ) :
''' If Order is in the CHAT states:
2022-01-07 19:22:07 +00:00
If user is buyer : mark FIAT SENT and settle escrow !
If User is the seller and FIAT is SENT : Pay buyer invoice ! '''
2022-01-07 18:22:52 +00:00
if order . status == Order . Status . CHA or order . status == Order . Status . FSE : # TODO Alternatively, if all collateral is locked? test out
# If buyer, settle escrow and mark fiat sent
if cls . is_buyer ( order , user ) :
2022-01-08 17:19:30 +00:00
if cls . settle_escrow ( order ) : ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
2022-01-07 18:22:52 +00:00
order . trade_escrow . status = LNPayment . Status . SETLED
order . status = Order . Status . FSE
order . is_fiat_sent = True
# If seller and fiat sent, pay buyer invoice
elif cls . is_seller ( order , user ) :
if not order . is_fiat_sent :
return False , { ' bad_request ' : ' You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer. ' }
2022-01-08 17:19:30 +00:00
# Make sure the trade escrow is at least as big as the buyer invoice
2022-01-09 20:05:19 +00:00
if order . trade_escrow . num_satoshis > order . buyer_invoice . num_satoshis :
2022-01-08 17:19:30 +00:00
return False , { ' bad_request ' : ' Woah, something broke badly. Report in the public channels, or open a Github Issue. ' }
2022-01-07 18:22:52 +00:00
# Double check the escrow is settled.
if LNNode . double_check_htlc_is_settled ( order . trade_escrow . payment_hash ) :
2022-01-08 17:19:30 +00:00
if cls . pay_buyer_invoice ( order ) : ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
2022-01-07 18:22:52 +00:00
order . status = Order . Status . PAY
order . buyer_invoice . status = LNPayment . Status . PAYING
else :
return False , { ' bad_request ' : ' You cannot confirm the fiat payment at this stage ' }
order . save ( )
2022-01-09 20:05:19 +00:00
return True , None