2022-10-20 20:57:46 +00:00
import math
2022-01-17 23:11:41 +00:00
from datetime import timedelta
2022-10-20 20:57:46 +00:00
2024-04-29 22:58:03 +00:00
from decouple import config , Csv
2023-05-01 10:30:53 +00:00
from django . contrib . auth . models import User
2022-06-07 22:14:56 +00:00
from django . db . models import Q , Sum
2022-10-20 20:57:46 +00:00
from django . utils import timezone
2022-01-06 16:54:37 +00:00
2022-10-20 20:57:46 +00:00
from api . lightning . node import LNNode
2023-05-01 10:30:53 +00:00
from api . models import Currency , LNPayment , MarketTick , OnchainPayment , Order
2024-10-01 19:35:57 +00:00
from api . tasks import send_devfund_donation , send_notification , nostr_send_order_event
2024-04-29 22:58:03 +00:00
from api . utils import get_minning_fee , validate_onchain_address , location_country
2023-04-24 11:05:52 +00:00
from chat . models import Message
2022-01-06 16:54:37 +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
2023-10-10 12:48:05 +00:00
MIN_ORDER_SIZE = config ( " MIN_ORDER_SIZE " , cast = int , default = 20_000 )
2024-01-09 14:36:27 +00:00
MAX_ORDER_SIZE = config ( " MAX_ORDER_SIZE " , cast = int , default = 500_000 )
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-07-21 13:19:47 +00:00
BLOCK_TIME = float ( config ( " BLOCK_TIME " ) )
2022-10-20 09:56:10 +00:00
MAX_MINING_NETWORK_SPEEDUP_EXPECTED = float (
config ( " MAX_MINING_NETWORK_SPEEDUP_EXPECTED " )
)
2022-01-06 16:54:37 +00:00
2024-05-01 02:46:08 +00:00
GEOBLOCKED_COUNTRIES = config ( " GEOBLOCKED_COUNTRIES " , cast = Csv ( ) , default = " " )
2024-04-29 22:58:03 +00:00
2022-05-30 13:20:39 +00:00
2022-10-20 09:56: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 ,
2022-04-29 18:54:20 +00:00
Order . Status . PAU ,
2022-02-17 19:50:10 +00:00
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 """
2022-10-20 09:56:10 +00:00
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 ,
2022-10-20 09:56:10 +00:00
{ " bad_request " : " You are already maker of an active order " } ,
2022-02-17 19:50:10 +00:00
queryset [ 0 ] ,
)
2022-01-17 23:22:44 +00:00
2022-10-20 09:56: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 ,
2022-10-20 09:56:10 +00:00
{ " bad_request " : " You are already taker of an active order " } ,
2022-02-17 19:50:10 +00:00
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-10-20 09:56:10 +00:00
queryset = Order . objects . filter (
Q ( maker = user ) | Q ( taker = user ) ,
status__in = [ Order . Status . FAI , Order . Status . PAY ] ,
)
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 ,
{
2022-10-20 09:56:10 +00:00
" bad_request " : " You are still pending a payment from a recent order "
2022-02-17 19:50:10 +00:00
} ,
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 :
2023-10-10 12:48:05 +00:00
if order . t0_satoshis > MAX_ORDER_SIZE :
2022-03-21 23:27:36 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Your order is too big. It is worth "
+ " {:,} " . format ( order . t0_satoshis )
+ " Sats now, but the limit is "
2023-10-10 12:48:05 +00:00
+ " {:,} " . format ( MAX_ORDER_SIZE )
2022-10-20 09:56:10 +00:00
+ " Sats "
2022-03-21 23:27:36 +00:00
}
2023-10-10 12:48:05 +00:00
if order . t0_satoshis < MIN_ORDER_SIZE :
2022-03-21 23:27:36 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Your order is too small. It is worth "
+ " {:,} " . format ( order . t0_satoshis )
+ " Sats now, but the limit is "
2023-10-10 12:48:05 +00:00
+ " {:,} " . format ( MIN_ORDER_SIZE )
2022-10-20 09:56:10 +00:00
+ " Sats "
2022-03-21 23:27:36 +00:00
}
elif order . has_range :
2022-10-20 09:56:10 +00:00
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 :
2022-03-21 23:27:36 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Maximum range amount must be at least 50 percent higher than the minimum amount "
2022-03-21 23:27:36 +00:00
}
2023-10-10 12:48:05 +00:00
elif max_sats > MAX_ORDER_SIZE :
2022-03-21 23:27:36 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Your order maximum amount is too big. It is worth "
+ " {:,} " . format ( int ( max_sats ) )
+ " Sats now, but the limit is "
2023-10-10 12:48:05 +00:00
+ " {:,} " . format ( MAX_ORDER_SIZE )
2022-10-20 09:56:10 +00:00
+ " Sats "
2022-03-21 23:27:36 +00:00
}
2023-10-10 12:48:05 +00:00
elif min_sats < MIN_ORDER_SIZE :
2022-03-21 23:27:36 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Your order minimum amount is too small. It is worth "
+ " {:,} " . format ( int ( min_sats ) )
+ " Sats now, but the limit is "
2023-10-10 12:48:05 +00:00
+ " {:,} " . format ( MIN_ORDER_SIZE )
2022-10-20 09:56:10 +00:00
+ " Sats "
2022-03-21 23:27:36 +00:00
}
2023-05-05 12:07:13 +00:00
elif min_sats < max_sats / 15 :
2022-03-21 23:27:36 +00:00
return False , {
2023-05-05 12:07:13 +00:00
" bad_request " : " Your order amount range is too large. Max amount can only be 15 times bigger than min amount "
2022-03-21 23:27:36 +00:00
}
2022-01-06 21:36:22 +00:00
return True , None
2022-01-10 12:10:32 +00:00
2024-04-29 22:58:03 +00:00
@classmethod
def validate_location ( cls , order ) - > bool :
if not ( order . latitude or order . longitude ) :
return True , None
country = location_country ( order . longitude , order . latitude )
if country in GEOBLOCKED_COUNTRIES :
return False , {
" bad_request " : f " The coordinator does not support orders in { country } "
}
else :
return True , None
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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " The amount specified is outside the range specified by the maker "
2022-03-22 17:49:57 +00:00
}
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 :
2022-10-20 09:56:10 +00:00
order . amount = amount
2022-01-10 12:10:32 +00:00
order . taker = user
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . TAK )
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-10-20 09:56:10 +00:00
seconds = order . t_to_expire ( Order . Status . TAK )
)
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " amount " , " taker " , " expires_at " ] )
2024-11-06 10:35:03 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2023-08-06 17:48:20 +00:00
order . log (
f " Taken by Robot( { user . robot . id } , { user . username } ) for { order . amount } fiat units "
)
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 (
2022-10-20 09:56:10 +00:00
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 (
2022-10-20 09:56:10 +00:00
is_taker and order . type == Order . Types . BUY
)
2022-02-17 19:50:10 +00:00
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 )
2022-10-20 09:56:10 +00:00
return ( float ( amount ) / premium_rate ) * 100 * 1000 * 1000
2022-03-21 23:27:36 +00:00
@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-10-20 20:53:51 +00:00
amount = order . amount if order . amount is not None else order . max_amount
2022-10-20 09:56:10 +00:00
satoshis_now = cls . calc_sats (
amount , order . currency . exchange_rate , order . premium
)
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-03-25 00:09:55 +00:00
amount = order . amount if not order . has_range else order . max_amount
2023-05-01 10:30:53 +00:00
order_rate = float ( amount ) / ( float ( order . satoshis ) / 100_000_000 )
2022-01-10 01:12:58 +00:00
premium = order_rate / exchange_rate - 1
2023-05-01 10:30:53 +00:00
premium = int ( premium * 10_000 ) / 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 (
2022-10-20 09:56:10 +00:00
price , significant_digits - int ( math . floor ( math . log10 ( abs ( price ) ) ) ) - 1
)
2022-02-17 19:50:10 +00:00
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 . 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-06-20 17:56:08 +00:00
# in any case, if order is_swap and there is an onchain_payment, cancel it.
2022-10-20 20:53:51 +00:00
if order . status not in does_not_expire :
2022-06-20 17:56:08 +00:00
cls . cancel_onchain_payment ( order )
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 :
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . EXP )
2022-04-29 18:54:20 +00:00
order . expiry_reason = Order . ExpiryReasons . NMBOND
2022-01-17 18:11:44 +00:00
cls . cancel_bond ( order . maker_bond )
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expiry_reason " ] )
order . log ( " Order expired while waiting for maker bond " )
order . log ( " Maker bond was cancelled " )
2022-01-16 21:54:42 +00:00
return True
2022-02-17 19:50:10 +00:00
2022-04-29 18:54:20 +00:00
elif order . status in [ Order . Status . PUB , Order . Status . PAU ] :
2022-01-16 18:32:34 +00:00
cls . return_bond ( order . maker_bond )
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . EXP )
2022-04-29 18:54:20 +00:00
order . expiry_reason = Order . ExpiryReasons . NTAKEN
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expiry_reason " ] )
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " order_expired_untaken " )
2023-08-06 17:48:20 +00:00
order . log ( " Order expired while public or paused " )
order . log ( " Maker bond was <b>unlocked</b> " )
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 )
2023-08-06 17:48:20 +00:00
2024-11-06 10:35:03 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2023-08-06 17:48:20 +00:00
order . log ( " Order expired while waiting for taker bond " )
order . log ( " Taker bond was cancelled " )
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 )
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . EXP )
2024-10-01 19:35:57 +00:00
order . expiry_reason = Order . ExpiryReasons . NESINV
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expiry_reason " ] )
order . log (
" Order expired while waiting for both buyer invoice and seller escrow "
)
order . log ( " Maker bond was <b>settled</b> " )
order . log ( " Taker bond was <b>settled</b> " )
2022-01-16 21:54:42 +00:00
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
2022-10-20 09:56:10 +00:00
try :
2022-02-20 14:38:29 +00:00
cls . cancel_escrow ( order )
2022-10-20 20:53:51 +00:00
except Exception :
2022-02-20 14:38:29 +00:00
pass
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . EXP )
2024-10-01 19:35:57 +00:00
order . expiry_reason = Order . ExpiryReasons . NESCRO
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expiry_reason " ] )
2022-03-07 21:46:52 +00:00
# Reward taker with part of the maker bond
2023-05-08 18:10:37 +00:00
cls . add_slashed_rewards ( order , order . maker_bond , order . taker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Order expired while waiting for escrow of the maker/seller " )
order . log ( " Maker bond was <b>settled</b> " )
order . log ( " Taker bond was <b>unlocked</b> " )
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
2022-10-20 09:56:10 +00:00
try :
2022-02-20 14:38:29 +00:00
cls . cancel_escrow ( order )
2022-10-20 20:53:51 +00:00
except Exception :
2022-02-20 14:38:29 +00:00
pass
2022-03-07 21:46:52 +00:00
taker_bond = order . taker_bond
2022-01-18 15:23:57 +00:00
cls . publish_order ( order )
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " order_published " )
2022-03-07 21:46:52 +00:00
# Reward maker with part of the taker bond
2023-05-08 18:10:37 +00:00
cls . add_slashed_rewards ( order , taker_bond , order . maker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Order expired while waiting for escrow of the taker/seller " )
order . log ( " Taker bond was <b>settled</b> " )
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 )
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . EXP )
2024-10-01 19:35:57 +00:00
order . expiry_reason = Order . ExpiryReasons . NINVOI
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expiry_reason " ] )
2022-03-07 21:46:52 +00:00
# Reward taker with part of the maker bond
2023-05-08 18:10:37 +00:00
cls . add_slashed_rewards ( order , order . maker_bond , order . taker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Order expired while waiting for invoice of the maker/buyer " )
order . log ( " Maker bond was <b>settled</b> " )
order . log ( " Taker bond was <b>unlocked</b> " )
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-18 15:23:57 +00:00
cls . publish_order ( order )
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " order_published " )
2022-03-07 21:46:52 +00:00
# Reward maker with part of the taker bond
2023-05-08 18:10:37 +00:00
cls . add_slashed_rewards ( order , taker_bond , order . maker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Order expired while waiting for invoice of the taker/buyer " )
order . log ( " Taker bond was <b>settled</b> " )
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 )
2023-08-06 17:48:20 +00:00
order . log (
" Order expired during chat and a dispute was opened automatically "
)
2022-01-16 21:54:42 +00:00
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 :
2023-05-01 10:30:53 +00:00
robot = order . taker . robot
robot . penalty_expiration = timezone . now ( ) + timedelta (
2022-10-20 09:56:10 +00:00
seconds = PENALTY_TIMEOUT
)
2023-05-08 18:10:37 +00:00
robot . save ( update_fields = [ " penalty_expiration " ] )
2022-01-12 00:02:17 +00:00
2022-01-17 18:11:44 +00:00
# Make order public again
2022-01-18 15:23:57 +00:00
cls . publish_order ( order )
2023-08-06 17:48:20 +00:00
order . log ( " Taker was kicked out of the order " )
2022-01-17 18:11:44 +00:00
return True
2022-01-12 00:02:17 +00:00
2023-04-24 11:05:52 +00:00
@classmethod
def automatic_dispute_resolution ( cls , order ) :
""" Simple case where a dispute can be solved with a
priori knowledge . For example , a dispute that opens
at expiration on an order where one of the participants
never sent a message on the chat and never marked ' fiat
sent ' . By solving the dispute automatically before
flagging it as dispute , we avoid having to settle the
bonds """
# If fiat has been marked as sent, automatic dispute
# resolution is not possible.
2023-05-09 13:21:40 +00:00
if order . is_fiat_sent and not order . reverted_fiat_sent :
2023-04-24 11:05:52 +00:00
return False
# If the order has not entered dispute due to time expire
# (a user triggered it), automatic dispute resolution is
# not possible.
if order . expires_at > = timezone . now ( ) :
return False
num_messages_taker = len (
Message . objects . filter ( order = order , sender = order . taker )
)
num_messages_maker = len (
Message . objects . filter ( order = order , sender = order . maker )
)
if num_messages_maker == num_messages_taker == 0 :
cls . return_escrow ( order )
cls . settle_bond ( order . maker_bond )
cls . settle_bond ( order . taker_bond )
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . DIS )
order . log ( " Maker bond was <b>settled</b> " )
order . log ( " Taker bond was <b>settled</b> " )
order . log (
" No robot wrote in the chat, the dispute cannot be solved automatically "
)
2023-04-24 11:05:52 +00:00
elif num_messages_maker == 0 :
cls . return_escrow ( order )
cls . settle_bond ( order . maker_bond )
cls . return_bond ( order . taker_bond )
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . MLD )
2023-09-15 09:39:40 +00:00
cls . add_slashed_rewards ( order , order . maker_bond , order . taker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Maker bond was <b>settled</b> " )
order . log ( " Taker bond was <b>unlocked</b> " )
order . log (
" <b>The dispute was solved automatically:</b> ' Maker lost dispute ' , the maker did not write in the chat "
)
2023-04-24 11:05:52 +00:00
2023-05-09 13:21:40 +00:00
elif num_messages_taker == 0 :
2023-04-24 11:05:52 +00:00
cls . return_escrow ( order )
2023-05-12 18:29:19 +00:00
cls . settle_bond ( order . taker_bond )
cls . return_bond ( order . maker_bond )
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . TLD )
2023-09-15 09:39:40 +00:00
cls . add_slashed_rewards ( order , order . taker_bond , order . maker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Maker bond was <b>unlocked</b> " )
order . log ( " Taker bond was <b>settled</b> " )
order . log (
" <b>The dispute was solved automatically:</b> ' Taker lost dispute ' , the maker did not write in the chat "
)
2023-04-24 11:05:52 +00:00
else :
return False
order . is_disputed = True
order . expires_at = timezone . now ( ) + timedelta (
seconds = order . t_to_expire ( Order . Status . DIS )
)
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " is_disputed " , " expires_at " ] )
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " dispute_opened " )
2023-04-24 11:05:52 +00:00
return True
2022-01-16 21:54:42 +00:00
@classmethod
def open_dispute ( cls , order , user = None ) :
2022-10-20 09:56:10 +00:00
# Always settle escrow and bonds during a dispute. Disputes
# can take long to resolve, it might trigger force closure
# 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-05-25 07:46:57 +00:00
valid_status_open_dispute = [
2022-05-16 06:47:22 +00:00
Order . Status . CHA ,
Order . Status . FSE ,
]
2022-05-25 07:46:57 +00:00
if order . status not in valid_status_open_dispute :
2022-10-20 09:56:10 +00:00
return False , {
" bad_request " : " You cannot open a dispute of this order at this stage "
}
2023-04-24 11:05:52 +00:00
automatically_solved = cls . automatic_dispute_resolution ( order )
if automatically_solved :
return True , None
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
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . DIS )
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-10-20 09:56:10 +00:00
seconds = order . t_to_expire ( Order . Status . DIS )
)
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " is_disputed " , " expires_at " ] )
2022-01-16 21:54:42 +00:00
2023-08-06 17:48:20 +00:00
# User could be None if a dispute is open automatically due to time expiration.
2022-10-20 20:53:51 +00:00
if user is not None :
2023-05-01 10:30:53 +00:00
robot = user . robot
robot . num_disputes = robot . num_disputes + 1
if robot . orders_disputes_started is None :
robot . orders_disputes_started = [ str ( order . id ) ]
2022-01-20 17:30:29 +00:00
else :
2023-05-01 10:30:53 +00:00
robot . orders_disputes_started = list (
robot . orders_disputes_started
2022-10-20 09:56:10 +00:00
) . append ( str ( order . id ) )
2023-05-08 18:10:37 +00:00
robot . save ( update_fields = [ " num_disputes " , " orders_disputes_started " ] )
2022-01-16 21:54:42 +00:00
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " dispute_opened " )
2023-08-06 17:48:20 +00:00
order . log (
f " Dispute was opened { f ' by Robot( { user . robot . id } , { user . username } ) ' if user else ' ' } "
)
order . log ( " Maker bond was <b>settled</b> " )
order . log ( " Taker bond was <b>settled</b> " )
2022-01-16 21:54:42 +00:00
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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Only orders in dispute accept dispute statements "
2022-02-17 19:50:10 +00:00
}
2022-01-16 21:54:42 +00:00
2023-05-01 10:30:53 +00:00
if len ( statement ) > 50_000 :
2022-02-17 19:50:10 +00:00
return False , {
2023-05-01 10:30:53 +00:00
" bad_statement " : " The statement and chat logs are longer than 50,000 characters "
2022-02-17 19:50:10 +00:00
}
2022-10-20 09:56:10 +00:00
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
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " maker_statement " ] )
2022-01-16 21:54:42 +00:00
else :
order . taker_statement = statement
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " taker_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-10-20 09:56:10 +00:00
if order . maker_statement not in [ None , " " ] and order . taker_statement not in [
None ,
" " ,
] :
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . WFR )
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-10-20 09:56:10 +00:00
seconds = order . t_to_expire ( Order . Status . WFR )
)
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " status " , " expires_at " ] )
2022-01-16 21:54:42 +00:00
2023-08-06 17:48:20 +00:00
order . log (
f " Dispute statement submitted by Robot( { user . robot . id } , { user . username } ) with length of { len ( statement ) } chars "
)
2022-01-16 21:54:42 +00:00
return True , None
2022-06-06 20:37:51 +00:00
def compute_swap_fee_rate ( balance ) :
2022-10-20 09:56:10 +00:00
shape = str ( config ( " SWAP_FEE_SHAPE " ) )
2022-06-06 20:37:51 +00:00
if shape == " linear " :
2023-10-10 12:48:05 +00:00
MIN_SWAP_FEE = config ( " MIN_SWAP_FEE " , cast = float , default = 0.01 )
2022-10-20 09:56:10 +00:00
MIN_POINT = float ( config ( " MIN_POINT " ) )
MAX_SWAP_FEE = float ( config ( " MAX_SWAP_FEE " ) )
MAX_POINT = float ( config ( " MAX_POINT " ) )
2022-06-11 13:12:09 +00:00
if float ( balance . onchain_fraction ) > MIN_POINT :
2022-06-06 20:37:51 +00:00
swap_fee_rate = MIN_SWAP_FEE
else :
slope = ( MAX_SWAP_FEE - MIN_SWAP_FEE ) / ( MAX_POINT - MIN_POINT )
2022-10-20 09:56:10 +00:00
swap_fee_rate = (
slope * ( balance . onchain_fraction - MAX_POINT ) + MAX_SWAP_FEE
)
2022-06-06 20:37:51 +00:00
2022-06-07 22:14:56 +00:00
elif shape == " exponential " :
2023-10-10 12:48:05 +00:00
MIN_SWAP_FEE = config ( " MIN_SWAP_FEE " , cast = float , default = 0.01 )
2022-10-20 09:56:10 +00:00
MAX_SWAP_FEE = float ( config ( " MAX_SWAP_FEE " ) )
SWAP_LAMBDA = float ( config ( " SWAP_LAMBDA " ) )
swap_fee_rate = MIN_SWAP_FEE + ( MAX_SWAP_FEE - MIN_SWAP_FEE ) * math . exp (
- SWAP_LAMBDA * float ( balance . onchain_fraction )
)
2022-06-07 22:14:56 +00:00
2022-06-11 13:12:09 +00:00
return swap_fee_rate * 100
2022-06-06 20:37:51 +00:00
@classmethod
2022-06-11 13:12:09 +00:00
def create_onchain_payment ( cls , order , user , preliminary_amount ) :
2022-10-20 09:56:10 +00:00
"""
2022-06-06 20:37:51 +00:00
Creates an empty OnchainPayment for order . payout_tx .
It sets the fees to be applied to this order if onchain Swap is used .
If the user submits a LN invoice instead . The returned OnchainPayment goes unused .
2022-10-20 09:56:10 +00:00
"""
2022-06-17 11:36:27 +00:00
# Make sure no invoice payout is attached to order
order . payout = None
# Create onchain_payment
2022-06-11 13:12:09 +00:00
onchain_payment = OnchainPayment . objects . create ( receiver = user )
2022-10-20 09:56:10 +00:00
2022-06-07 22:14:56 +00:00
# Compute a safer available onchain liquidity: (confirmed_utxos - reserve - pending_outgoing_txs))
# Accounts for already committed outgoing TX for previous users.
confirmed = onchain_payment . balance . onchain_confirmed
2023-05-22 14:56:15 +00:00
# We assume a reserve of 300K Sats (3 times higher than LND's default anchor reserve)
reserve = 300_000
2022-10-20 09:56:10 +00:00
pending_txs = OnchainPayment . objects . filter (
2023-03-14 17:23:11 +00:00
status__in = [ OnchainPayment . Status . VALID , OnchainPayment . Status . QUEUE ]
2022-10-20 09:56:10 +00:00
) . aggregate ( Sum ( " num_satoshis " ) ) [ " num_satoshis__sum " ]
2022-10-20 20:53:51 +00:00
if pending_txs is None :
2022-06-11 13:12:09 +00:00
pending_txs = 0
2022-10-20 09:56:10 +00:00
2022-06-07 22:14:56 +00:00
available_onchain = confirmed - reserve - pending_txs
2022-10-20 09:56:10 +00:00
if (
preliminary_amount > available_onchain
) : # Not enough onchain balance to commit for this swap.
2022-06-07 22:14:56 +00:00
return False
2024-01-08 14:13:37 +00:00
suggested_mining_fee_rate = get_minning_fee ( " suggested " , preliminary_amount )
2022-06-11 13:12:09 +00:00
2023-05-08 18:10:37 +00:00
# Hardcap mining fee suggested at 1000 sats/vbyte
if suggested_mining_fee_rate > 1000 :
suggested_mining_fee_rate = 1000
2022-06-11 13:12:09 +00:00
2023-05-08 18:10:37 +00:00
onchain_payment . suggested_mining_fee_rate = max ( 2.05 , suggested_mining_fee_rate )
2022-10-20 09:56:10 +00:00
onchain_payment . swap_fee_rate = cls . compute_swap_fee_rate (
onchain_payment . balance
)
2022-06-06 20:37:51 +00:00
onchain_payment . save ( )
order . payout_tx = onchain_payment
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " payout_tx " ] )
2023-08-06 17:48:20 +00:00
order . log (
f " Empty OnchainPayment( { order . payout_tx . id } , { order . payout_tx } ) was created. Available onchain balance is { available_onchain } Sats "
)
2022-06-07 22:14:56 +00:00
return True
2022-06-06 20:37:51 +00:00
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,
2022-06-06 20:37:51 +00:00
that is the final trade amount set at Taker Bond time
Adds context for onchain swap .
"""
if not cls . is_buyer ( order , user ) :
return False , None
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-06-06 20:37:51 +00:00
context = { }
# context necessary for the user to submit a LN invoice
2022-10-20 09:56:10 +00:00
context [ " invoice_amount " ] = round (
2023-05-05 12:30:25 +00:00
order . last_satoshis - fee_sats
2022-10-20 09:56:10 +00:00
) # Trading fee to buyer is charged here.
2022-06-06 20:37:51 +00:00
# context necessary for the user to submit an onchain address
2023-05-01 10:30:53 +00:00
MIN_SWAP_AMOUNT = config ( " MIN_SWAP_AMOUNT " , cast = int , default = 20_000 )
MAX_SWAP_AMOUNT = config ( " MAX_SWAP_AMOUNT " , cast = int , default = 500_000 )
2022-06-06 20:37:51 +00:00
if context [ " invoice_amount " ] < MIN_SWAP_AMOUNT :
context [ " swap_allowed " ] = False
2024-07-15 10:33:09 +00:00
context [ " swap_failure_reason " ] = (
f " Order amount is smaller than the minimum swap available of { MIN_SWAP_AMOUNT } Sats "
)
2023-08-06 17:48:20 +00:00
order . log (
f " Onchain payment option was not offered: amount is smaller than the minimum swap available of { MIN_SWAP_AMOUNT } Sats " ,
level = " WARN " ,
)
2023-03-14 19:54:31 +00:00
return True , context
elif context [ " invoice_amount " ] > MAX_SWAP_AMOUNT :
context [ " swap_allowed " ] = False
2024-07-15 10:33:09 +00:00
context [ " swap_failure_reason " ] = (
f " Order amount is bigger than the maximum swap available of { MAX_SWAP_AMOUNT } Sats "
)
2023-08-06 17:48:20 +00:00
order . log (
f " Onchain payment option was not offered: amount is bigger than the maximum swap available of { MAX_SWAP_AMOUNT } Sats " ,
level = " WARN " ,
)
2022-06-06 20:37:51 +00:00
return True , context
2023-03-14 19:54:31 +00:00
if config ( " DISABLE_ONCHAIN " , cast = bool , default = True ) :
2022-06-16 15:31:30 +00:00
context [ " swap_allowed " ] = False
2023-08-06 17:48:20 +00:00
context [ " swap_failure_reason " ] = " On-the-fly submarine swaps are disabled "
order . log (
" Onchain payment option was not offered: on-the-fly submarine swaps are disabled "
)
2022-06-16 15:31:30 +00:00
return True , context
2022-10-20 20:53:51 +00:00
if order . payout_tx is None :
2022-06-07 22:14:56 +00:00
# Creates the OnchainPayment object and checks node balance
2022-10-20 09:56:10 +00:00
valid = cls . create_onchain_payment (
order , user , preliminary_amount = context [ " invoice_amount " ]
)
2023-08-06 17:48:20 +00:00
order . log (
f " Suggested mining fee is { order . payout_tx . suggested_mining_fee_rate } Sats/vbyte, the swap fee rate is { order . payout_tx . swap_fee_rate } % "
)
2022-06-07 22:14:56 +00:00
if not valid :
context [ " swap_allowed " ] = False
2024-07-15 10:33:09 +00:00
context [ " swap_failure_reason " ] = (
" Not enough onchain liquidity available to offer a swap "
)
2023-08-06 17:48:20 +00:00
order . log (
" Onchain payment option was not offered: onchain liquidity available to offer a swap " ,
level = " WARN " ,
)
2022-06-07 22:14:56 +00:00
return True , context
2022-06-06 20:37:51 +00:00
context [ " swap_allowed " ] = True
2023-11-17 12:57:37 +00:00
context [ " suggested_mining_fee_rate " ] = float (
order . payout_tx . suggested_mining_fee_rate
)
2022-06-06 20:37:51 +00:00
context [ " swap_fee_rate " ] = order . payout_tx . swap_fee_rate
2022-01-07 11:31:33 +00:00
2022-06-06 20:37:51 +00:00
return True , context
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,
2022-10-20 09:56:10 +00:00
that is the final trade amount set at Taker Bond time """
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 )
2022-10-20 09:56:10 +00:00
fee_sats = order . last_satoshis * fee_fraction
2022-03-05 20:51:16 +00:00
2022-03-03 15:40:56 +00:00
if cls . is_seller ( order , user ) :
2022-10-20 09:56:10 +00:00
escrow_amount = round (
2023-05-05 12:30:25 +00:00
order . last_satoshis + fee_sats
2022-10-20 09:56:10 +00:00
) # Trading fee to seller is charged here.
2022-03-03 15:40:56 +00:00
return True , { " escrow_amount " : escrow_amount }
2022-06-13 22:27:09 +00:00
@classmethod
def update_address ( cls , order , user , address , mining_fee_rate ) :
# Empty address?
if not address :
2023-08-06 17:48:20 +00:00
return False , { " bad_address " : " You submitted an empty address " }
2022-06-13 22:27:09 +00:00
# only the buyer can post a buyer address
if not cls . is_buyer ( order , user ) :
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Only the buyer of this order can provide a payout address. "
2022-06-13 22:27:09 +00:00
}
# not the right time to submit
2023-03-17 20:55:49 +00:00
if not (
order . taker_bond . status
== order . maker_bond . status
== LNPayment . Status . LOCKED
) or order . status not in [ Order . Status . WFI , Order . Status . WF2 ] :
2023-08-06 17:48:20 +00:00
order . log (
f " Robot( { user . robot . id } , { user . username } ) attempted to submit an address while the order was in status { order . status } " ,
level = " ERROR " ,
)
2023-03-17 20:55:49 +00:00
return False , { " bad_request " : " You cannot submit an address now. " }
# not a valid address
2022-06-17 11:36:27 +00:00
valid , context = validate_onchain_address ( address )
if not valid :
2023-08-06 17:48:20 +00:00
order . log ( f " The address { address } is not valid " , level = " WARN " )
2022-06-17 11:36:27 +00:00
return False , context
2023-03-17 20:55:49 +00:00
num_satoshis = cls . payout_amount ( order , user ) [ 1 ] [ " invoice_amount " ]
2022-06-13 22:27:09 +00:00
if mining_fee_rate :
# not a valid mining fee
2024-01-08 14:13:37 +00:00
min_mining_fee_rate = get_minning_fee ( " minimum " , num_satoshis )
2023-03-17 20:55:49 +00:00
min_mining_fee_rate = max ( 2 , min_mining_fee_rate )
if float ( mining_fee_rate ) < min_mining_fee_rate :
2023-08-06 17:48:20 +00:00
order . log (
f " The onchain fee { float ( mining_fee_rate ) } Sats/vbytes proposed by Robot( { user . robot . id } , { user . username } ) is less than the current minimum mining fee { min_mining_fee_rate } Sats " ,
level = " WARN " ,
)
2022-06-13 22:27:09 +00:00
return False , {
2023-03-17 20:55:49 +00:00
" bad_address " : f " The mining fee is too low. Must be higher than { min_mining_fee_rate } Sat/vbyte "
2022-06-13 22:27:09 +00:00
}
2023-05-05 19:44:18 +00:00
elif float ( mining_fee_rate ) > 500 :
2023-08-06 17:48:20 +00:00
order . log (
f " The onchain fee { float ( mining_fee_rate ) } Sats/vbytes proposed by Robot( { user . robot . id } , { user . username } ) is higher than the absolute maximum mining fee 500 Sats " ,
level = " WARN " ,
)
2022-06-13 22:27:09 +00:00
return False , {
2023-05-05 19:44:18 +00:00
" bad_address " : " The mining fee is too high, must be less than 500 Sats/vbyte "
2022-06-13 22:27:09 +00:00
}
order . payout_tx . mining_fee_rate = float ( mining_fee_rate )
2023-05-05 19:44:18 +00:00
# If not mining fee provider use backend's suggested fee rate
2022-06-13 22:27:09 +00:00
else :
order . payout_tx . mining_fee_rate = order . payout_tx . suggested_mining_fee_rate
tx = order . payout_tx
tx . address = address
2023-05-05 19:44:18 +00:00
tx . mining_fee_sats = int ( tx . mining_fee_rate * 280 )
2023-03-17 20:55:49 +00:00
tx . num_satoshis = num_satoshis
2022-10-20 09:56:10 +00:00
tx . sent_satoshis = int (
float ( tx . num_satoshis )
- float ( tx . num_satoshis ) * float ( tx . swap_fee_rate ) / 100
- float ( tx . mining_fee_sats )
)
2023-05-05 19:44:18 +00:00
if float ( tx . sent_satoshis ) < 20_000 :
2023-08-06 17:48:20 +00:00
order . log (
f " The onchain Sats to be sent ( { float ( tx . sent_satoshis ) } ) are below the dust limit of 20,000 Sats " ,
level = " WARN " ,
)
2023-05-05 19:44:18 +00:00
return False , {
" bad_address " : " The amount remaining after subtracting mining fee is close to dust limit. "
}
2022-06-13 22:27:09 +00:00
tx . status = OnchainPayment . Status . VALID
tx . save ( )
order . is_swap = True
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " is_swap " ] )
2022-06-13 22:27:09 +00:00
2023-08-06 17:48:20 +00:00
order . log (
f " Robot( { user . robot . id } , { user . username } ) added an onchain address OnchainPayment( { tx . id } , { address [ : 6 ] } ... { address [ - 4 : ] } ) as payout method. Amount to be sent is { tx . sent_satoshis } Sats, mining fee is { tx . mining_fee_sats } Sats "
)
2022-06-13 22:27:09 +00:00
cls . move_state_updated_payout_method ( order )
return True , None
2022-01-06 16:54:37 +00:00
@classmethod
2022-11-24 17:42:30 +00:00
def update_invoice ( cls , order , user , invoice , routing_budget_ppm ) :
2022-05-28 13:05:26 +00:00
# Empty invoice?
if not invoice :
2023-08-06 17:48:20 +00:00
order . log (
f " Robot( { user . robot . id } , { user . username } ) submitted an empty invoice " ,
level = " WARN " ,
)
2022-10-20 09:56:10 +00:00
return False , { " bad_invoice " : " You submitted an empty invoice " }
2022-01-07 19:22:07 +00:00
# only the buyer can post a buyer invoice
if not cls . is_buyer ( order , user ) :
2022-02-17 19:50:10 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Only the buyer of this order can provide a buyer invoice. "
2022-02-17 19:50:10 +00:00
}
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. " }
2022-10-20 09:56:10 +00:00
if (
2022-10-22 14:23:22 +00:00
not (
order . taker_bond . status
== order . maker_bond . status
== LNPayment . Status . LOCKED
)
2022-10-20 09:56:10 +00:00
and not order . status == Order . Status . FAI
) :
2022-02-17 19:50:10 +00:00
return False , {
2023-08-06 17:48:20 +00:00
" bad_request " : " You cannot submit an invoice while bonds are not locked. "
2022-02-17 19:50:10 +00:00
}
2022-03-20 23:32:25 +00:00
if order . status == Order . Status . FAI :
if order . payout . status != LNPayment . Status . EXPIRE :
return False , {
2024-05-05 19:58:18 +00:00
" bad_invoice " : " You can only submit an invoice after expiration or 3 failed attempts "
2022-03-20 23:32:25 +00:00
}
2022-02-17 19:50:10 +00:00
2022-06-17 11:36:27 +00:00
# cancel onchain_payout if existing
2022-06-20 17:56:08 +00:00
cls . cancel_onchain_payment ( order )
2022-06-17 11:36:27 +00:00
2022-02-17 19:50:10 +00:00
num_satoshis = cls . payout_amount ( order , user ) [ 1 ] [ " invoice_amount " ]
2022-11-24 17:42:30 +00:00
routing_budget_sats = float ( num_satoshis ) * (
2023-05-01 10:30:53 +00:00
float ( routing_budget_ppm ) / 1_000_000
2022-11-24 17:42:30 +00:00
)
num_satoshis = int ( num_satoshis - routing_budget_sats )
payout = LNNode . validate_ln_invoice ( invoice , num_satoshis , routing_budget_ppm )
2022-01-12 00:02:17 +00:00
2022-02-17 19:50:10 +00:00
if not payout [ " valid " ] :
2022-10-22 14:23:22 +00:00
return False , payout [ " context " ]
2022-01-06 22:39:59 +00:00
2024-05-05 19:58:18 +00:00
if order . payout :
if order . payout . payment_hash == payout [ " payment_hash " ] :
return False , { " bad_invoice " : " You must submit a NEW invoice " }
order . payout = LNPayment . objects . create (
2022-02-17 19:50:10 +00:00
concept = LNPayment . Concepts . PAYBUYER ,
type = LNPayment . Types . NORM ,
sender = User . objects . get ( username = ESCROW_USERNAME ) ,
receiver = user ,
2022-11-24 17:42:30 +00:00
routing_budget_ppm = routing_budget_ppm ,
routing_budget_sats = routing_budget_sats ,
2024-05-05 19:58:18 +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-02-17 19:50:10 +00:00
)
2022-01-06 22:39:59 +00:00
2022-06-13 22:27:09 +00:00
order . is_swap = False
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " payout " , " is_swap " ] )
2022-06-13 22:27:09 +00:00
2023-08-06 17:48:20 +00:00
order . log (
f " Robot( { user . robot . id } , { user . username } ) added the invoice LNPayment( { order . payout . payment_hash } , { order . payout . payment_hash } ) as payout method. Amount to be sent is { order . payout . num_satoshis } Sats, routing budget is { order . payout . routing_budget_sats } Sats ( { order . payout . routing_budget_ppm } ppm) "
)
2022-06-13 22:27:09 +00:00
cls . move_state_updated_payout_method ( order )
return True , None
@classmethod
2022-10-20 09:56:10 +00:00
def move_state_updated_payout_method ( cls , order ) :
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 :
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . CHA )
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-10-20 09:56:10 +00:00
seconds = order . t_to_expire ( Order . Status . CHA )
)
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " fiat_exchange_starts " )
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-06-19 06:09:21 +00:00
elif 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.
2022-10-20 20:53:51 +00:00
if order . trade_escrow is None :
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . WFE )
2022-01-30 15:50:22 +00:00
# If the escrow is locked move to Chat.
elif order . trade_escrow . status == LNPayment . Status . LOCKED :
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . CHA )
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-10-20 09:56:10 +00:00
seconds = order . t_to_expire ( Order . Status . CHA )
)
2024-10-01 19:35:57 +00:00
send_notification . delay (
order_id = order . id , message = " fiat_exchange_starts "
)
2022-01-06 22:39:59 +00:00
else :
2023-08-06 17:48:20 +00:00
order . update_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-06-19 06:09:21 +00:00
elif order . status == Order . Status . FAI :
2022-10-20 09:56:10 +00:00
if LNNode . double_check_htlc_is_settled ( order . trade_escrow . payment_hash ) :
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . PAY )
2022-02-04 18:07:09 +00:00
order . payout . status = LNPayment . Status . FLIGHT
order . payout . routing_attempts = 0
2023-05-08 18:10:37 +00:00
order . payout . save ( update_fields = [ " status " , " routing_attempts " ] )
2022-10-20 09:56:10 +00:00
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expires_at " ] )
2022-06-13 22:27:09 +00:00
return True
2022-01-06 20:33:40 +00:00
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 """
2023-05-01 10:30:53 +00:00
if user . robot . penalty_expiration :
if user . robot . penalty_expiration > timezone . now ( ) :
time_out = ( user . robot . 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-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 . 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-10-20 20:53:51 +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 )
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . UCA )
order . log ( " Order expired while waiting for maker bond " )
order . log ( " Maker bond was cancelled " )
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-06 22:39:59 +00:00
2022-10-20 20:53:51 +00:00
# 2.a) When maker cancels after bond
#
2023-08-06 17:48:20 +00:00
# The order disapears from book and goes to cancelled. If strict, maker is charged the bond
2022-10-20 20:53:51 +00:00
# to prevent DDOS on the LN node and order book. If not strict, maker is returned
# the bond (more user friendly).
2022-10-20 09:56:10 +00:00
elif (
order . status in [ Order . Status . PUB , Order . Status . PAU ] 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)
2022-10-20 09:56:10 +00:00
if cls . return_bond ( order . maker_bond ) :
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . UCA )
2024-10-01 19:35:57 +00:00
send_notification . delay (
order_id = order . id , message = " public_order_cancelled "
)
2023-08-06 17:48:20 +00:00
order . log ( " Order cancelled by maker while public or paused " )
order . log ( " Maker bond was <b>unlocked</b> " )
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-05-05 13:58:13 +00:00
return True , None
2022-10-20 20:53:51 +00:00
# 2.b) When maker cancels after bond and before taker bond is locked
#
# The order dissapears from book and goes to cancelled.
# The bond maker bond is returned.
2022-05-05 13:58:13 +00:00
elif order . status == Order . Status . TAK and order . maker == user :
# Return the maker bond (Maker gets returned the bond for cancelling public order)
2022-10-20 09:56:10 +00:00
if cls . return_bond ( order . maker_bond ) :
2022-05-05 13:58:13 +00:00
cls . cancel_bond ( order . taker_bond )
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . UCA )
2024-10-01 19:35:57 +00:00
send_notification . delay (
order_id = order . id , message = " public_order_cancelled "
)
2023-08-06 17:48:20 +00:00
order . log ( " Order cancelled by maker before the taker locked the bond " )
order . log ( " Maker bond was <b>unlocked</b> " )
order . log ( " Taker bond was <b>cancelled</b> " )
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-06 20:33:40 +00:00
2022-10-20 20:53:51 +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 )
2023-08-06 17:48:20 +00:00
order . log ( " Taker cancelled before locking the bond " )
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-06 20:33:40 +00:00
2022-10-20 20:53:51 +00:00
# 4) When taker or maker cancel after bond (before escrow)
#
# 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-02-17 19:50:10 +00:00
2022-10-20 20:53:51 +00:00
# 4.a) When maker cancel after bond (before escrow)
# The order into cancelled status if maker cancels.
2022-10-20 09:56:10 +00:00
elif (
order . status in [ Order . Status . WF2 , Order . Status . WFE ] and order . maker == user
) :
2022-06-20 17:56:08 +00:00
# cancel onchain payment if existing
cls . cancel_onchain_payment ( order )
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
2023-08-06 17:48:20 +00:00
cls . cancel_escrow ( order )
2022-06-20 17:56:08 +00:00
2022-01-11 14:36:43 +00:00
if valid :
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . UCA )
2022-03-07 21:46:52 +00:00
# Reward taker with part of the maker bond
2023-05-08 18:10:37 +00:00
cls . add_slashed_rewards ( order , order . maker_bond , order . taker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Maker cancelled before escrow was locked " )
order . log ( " Maker bond was <b>settled</b> " )
order . log ( " Taker bond was <b>unlocked</b> " )
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-11 14:36:43 +00:00
2022-10-20 20:53:51 +00:00
# 4.b) When taker cancel after bond (before escrow)
# The order into cancelled status if mtker cancels.
2022-10-20 09:56:10 +00:00
elif (
order . status in [ Order . Status . WF2 , Order . Status . WFE ] and order . taker == user
) :
2022-06-20 17:56:08 +00:00
# cancel onchain payment if existing
cls . cancel_onchain_payment ( order )
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 :
2023-05-08 18:10:37 +00:00
taker_bond = order . taker_bond
2022-01-18 15:23:57 +00:00
cls . publish_order ( order )
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " order_published " )
2022-03-07 21:46:52 +00:00
# Reward maker with part of the taker bond
2023-05-08 18:10:37 +00:00
cls . add_slashed_rewards ( order , taker_bond , order . maker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Taker cancelled before escrow was locked " )
order . log ( " Taker bond was <b>settled</b> " )
order . log ( " Maker bond was <b>unlocked</b> " )
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-01-12 00:02:17 +00:00
return True , None
2022-01-11 14:36:43 +00:00
2022-10-20 20:53:51 +00:00
# 5) When trade collateral has been posted (after escrow)
#
# Always goes to CCA status. Collaboration is needed.
# When a user asks for cancel, 'order.m/t/aker_asked_cancel' goes True.
# When the second user asks for cancel. Order is totally cancelled.
# Must have a small cost for both parties to prevent node DDOS.
2022-10-20 09:56:10 +00:00
elif order . status in [ Order . Status . WFI , Order . Status . CHA ] :
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 )
2023-08-06 17:48:20 +00:00
order . log (
f " Taker Robot( { user . robot . id } , { user . username } ) accepted the collaborative cancellation "
)
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-01-23 19:02:25 +00:00
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 )
2023-08-06 17:48:20 +00:00
order . log (
f " Maker Robot( { user . robot . id } , { user . username } ) accepted the collaborative cancellation "
)
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-01-23 19:02:25 +00:00
return True , None
# Otherwise just make true the asked for cancel flags
elif user == order . taker :
order . taker_asked_cancel = True
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " taker_asked_cancel " ] )
2023-08-06 17:48:20 +00:00
order . log (
f " Taker Robot( { user . robot . id } , { user . username } ) asked for collaborative cancellation "
)
2022-01-23 19:02:25 +00:00
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
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " maker_asked_cancel " ] )
2023-08-06 17:48:20 +00:00
order . log (
f " Maker Robot( { user . robot . id } , { user . username } ) asked for collaborative cancellation "
)
2022-01-23 19:02:25 +00:00
return True , None
2022-01-06 22:39:59 +00:00
else :
2023-08-06 17:48:20 +00:00
order . log (
f " Cancel request was sent by Robot( { user . robot . id } , { user . username } ) on an invalid status { order . status } : <i> { Order . Status ( order . status ) . label } </i> "
)
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 ) :
2022-10-20 20:53:51 +00:00
if order . status not in [ Order . Status . WFI , Order . Status . CHA ] :
2022-06-04 21:26:53 +00:00
return
2022-06-20 17:56:08 +00:00
# cancel onchain payment if existing
cls . cancel_onchain_payment ( order )
2022-01-23 19:02:25 +00:00
cls . return_bond ( order . maker_bond )
cls . return_bond ( order . taker_bond )
cls . return_escrow ( order )
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . CCA )
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " collaborative_cancelled " )
2023-08-06 17:48:20 +00:00
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2023-08-06 17:48:20 +00:00
order . log ( " Order was collaboratively cancelled " )
order . log ( " Maker bond was <b>unlocked</b> " )
order . log ( " Taker bond was <b>unlocked</b> " )
order . log ( " Trade escrow was <b>unlocked</b> " )
2022-01-23 19:02:25 +00:00
return
2022-03-30 20:01:26 +00:00
@classmethod
def publish_order ( cls , order ) :
2024-10-01 19:35:57 +00:00
order . status = Order . Status . PUB
2022-02-17 19:50:10 +00:00
order . expires_at = order . created_at + timedelta (
2022-10-20 09:56:10 +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-03-30 20:01:26 +00:00
order . last_satoshis = cls . satoshis_now ( order )
2022-07-18 12:53:49 +00:00
order . last_satoshis_time = timezone . now ( )
2023-05-08 18:10:37 +00:00
# clear fields in case of re-publishing after expiry
order . taker = None
order . taker_bond = None
order . trade_escrow = None
order . payout = None
order . payout_tx = None
order . save ( ) # update all fields
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2024-07-01 16:00:17 +00:00
2023-08-06 17:48:20 +00:00
order . log ( f " Order( { order . id } , { str ( order ) } ) is public in the order book " )
2022-01-18 15:23:57 +00:00
return
2022-01-17 23:11:41 +00:00
2022-07-21 13:19:47 +00:00
def compute_cltv_expiry_blocks ( order , invoice_concept ) :
2022-10-20 09:56:10 +00:00
""" Computes timelock CLTV expiry of the last hop in blocks for hodl invoices
2022-07-21 13:19:47 +00:00
invoice_concepts ( str ) : maker_bond , taker_bond , trade_escrow
2022-10-20 09:56:10 +00:00
"""
2022-07-21 13:19:47 +00:00
# Every invoice_concept must be locked by at least the fiat exchange duration
2022-07-22 15:05:47 +00:00
# Every invoice must also be locked for deposit_time (order.escrow_duration or WFE status)
2022-07-21 13:19:47 +00:00
cltv_expiry_secs = order . t_to_expire ( Order . Status . CHA )
2022-07-22 15:05:47 +00:00
cltv_expiry_secs + = order . t_to_expire ( Order . Status . WFE )
2022-07-21 13:19:47 +00:00
# Maker bond must also be locked for the full public duration plus the taker bond locking time
if invoice_concept == " maker_bond " :
cltv_expiry_secs + = order . t_to_expire ( Order . Status . PUB )
cltv_expiry_secs + = order . t_to_expire ( Order . Status . TAK )
# Add a safety marging by multiplying by the maxium expected mining network speed up
safe_cltv_expiry_secs = cltv_expiry_secs * MAX_MINING_NETWORK_SPEEDUP_EXPECTED
# Convert to blocks using assummed average block time (~8 mins/block)
cltv_expiry_blocks = int ( safe_cltv_expiry_secs / ( BLOCK_TIME * 60 ) )
return cltv_expiry_blocks
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-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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Invoice expired. You did not confirm publishing the order in time. Make a new order. "
2022-02-17 19:50:10 +00:00
}
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 :
2023-05-08 18:10:37 +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-07-18 12:53:49 +00:00
order . last_satoshis_time = timezone . now ( )
2022-10-20 09:56:10 +00:00
bond_satoshis = int ( order . last_satoshis * order . bond_size / 100 )
2022-01-11 14:36:43 +00:00
2023-05-01 10:30:53 +00:00
if user . robot . wants_stealth :
2024-10-16 02:28:58 +00:00
description = f " { config ( " NODE_ALIAS " ) } - Payment reference: { order . reference } . This payment WILL FREEZE IN YOUR WALLET, check on RoboSats if the lock was successful. It will be unlocked (fail) unless you cheat or cancel unilaterally. "
2022-08-12 17:41:06 +00:00
else :
2024-10-16 02:28:58 +00:00
description = f " { config ( " NODE_ALIAS " ) } - Publishing ' { str ( order ) } ' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on RoboSats if the lock was successful. It will be unlocked (fail) 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-10-20 09:56:10 +00:00
cltv_expiry_blocks = cls . compute_cltv_expiry_blocks ( order , " maker_bond " ) ,
2023-05-22 14:56:15 +00:00
order_id = order . id ,
lnpayment_concept = LNPayment . Concepts . MAKEBOND . label ,
time = int ( timezone . now ( ) . timestamp ( ) ) ,
2022-02-17 19:50:10 +00:00
)
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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware. "
2022-02-17 19:50:10 +00:00
}
2022-07-21 13:19:47 +00:00
elif " wallet locked " in str ( e ) :
2022-02-17 19:50:10 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " This is weird, RoboSats ' lightning wallet is locked. Check in the Telegram group, maybe the staff has died. "
2022-02-17 19:50:10 +00:00
}
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
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " last_satoshis " , " last_satoshis_time " , " maker_bond " ] )
2023-08-06 17:48:20 +00:00
order . log (
f " Maker bond LNPayment( { order . maker_bond . payment_hash } , { str ( order . maker_bond ) } ) was created "
)
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 )
2022-07-18 12:53:49 +00:00
order . last_satoshis_time = timezone . now ( )
2022-02-17 19:50:10 +00:00
2022-10-20 09:56:10 +00:00
# With the bond confirmation the order is extended 'public_order_duration' hours
2022-03-05 12:21:01 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-10-20 09:56:10 +00:00
seconds = order . t_to_expire ( Order . Status . WF2 )
)
2024-10-01 19:35:57 +00:00
order . status = Order . Status . WF2
2023-05-08 18:10:37 +00:00
order . save (
update_fields = [
2023-09-11 15:23:11 +00:00
" status " ,
2023-05-08 18:10:37 +00:00
" last_satoshis " ,
" last_satoshis_time " ,
" expires_at " ,
]
)
2022-03-05 12:21:01 +00:00
2023-09-11 15:23:11 +00:00
order . taker_bond . status = LNPayment . Status . LOCKED
order . taker_bond . save ( update_fields = [ " status " ] )
2023-05-01 10:30:53 +00:00
# Both users robots are added one more contract // Unsafe can add more than once.
order . maker . robot . total_contracts + = 1
order . taker . robot . total_contracts + = 1
2023-05-08 18:10:37 +00:00
order . maker . robot . save ( update_fields = [ " total_contracts " ] )
order . taker . robot . save ( update_fields = [ " total_contracts " ] )
2022-10-20 09:56:10 +00:00
2022-03-05 12:19:56 +00:00
# Log a market tick
try :
2023-08-06 17:48:20 +00:00
market_tick = MarketTick . log_a_tick ( order )
order . log (
f " New Market Tick logged as MarketTick( { market_tick . id } , { market_tick } ) "
)
2022-10-20 20:53:51 +00:00
except Exception :
2022-03-05 12:19:56 +00:00
pass
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " order_taken_confirmed " )
2024-07-15 10:33:09 +00:00
2024-08-26 13:49:03 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2024-09-16 14:05:21 +00:00
2023-08-06 17:48:20 +00:00
order . log (
2023-09-14 09:31:42 +00:00
f " <b>Contract formalized.</b> Maker: Robot( { order . maker . robot . id } , { order . maker } ). Taker: Robot( { order . taker . robot . id } , { order . taker } ). API median price { order . currency . exchange_rate } { dict ( Currency . currency_choices ) [ order . currency . currency ] } /BTC. Premium is { order . premium } %. Contract size { order . last_satoshis } Sats "
2023-08-06 17:48:20 +00:00
)
2022-02-17 19:50:10 +00:00
return True
2022-01-17 23:11:41 +00:00
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-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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Invoice expired. You did not confirm taking the order in time. "
2022-02-17 19:50:10 +00:00
}
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 :
2023-05-08 18:10:37 +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-07-18 12:53:49 +00:00
order . last_satoshis_time = timezone . now ( )
2022-10-20 09:56:10 +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 "
2023-05-01 10:30:53 +00:00
if user . robot . wants_stealth :
2024-10-16 02:30:27 +00:00
description = f " { config ( " NODE_ALIAS " ) } - Payment reference: { order . reference } . This payment WILL FREEZE IN YOUR WALLET, check on RoboSats if the lock was successful. It will be unlocked (fail) unless you cheat or cancel unilaterally. "
2022-08-12 17:41:06 +00:00
else :
description = (
2024-10-16 02:30:27 +00:00
f " { config ( " NODE_ALIAS " ) } - Taking ' Order { order . id } ' { pos_text } BTC for { str ( float ( order . amount ) ) + Currency . currency_dict [ str ( order . currency . currency ) ] } "
2023-09-08 15:42:04 +00:00
+ " - Taker bond - This payment WILL FREEZE IN YOUR WALLET, check on RoboSats if the lock was successful. It will be unlocked (fail) unless you cheat or cancel unilaterally. "
2022-08-12 17:41:06 +00:00
)
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-10-20 09:56:10 +00:00
cltv_expiry_blocks = cls . compute_cltv_expiry_blocks ( order , " taker_bond " ) ,
2023-05-22 14:56:15 +00:00
order_id = order . id ,
lnpayment_concept = LNPayment . Concepts . TAKEBOND . label ,
time = int ( timezone . now ( ) . timestamp ( ) ) ,
2022-02-17 19:50:10 +00:00
)
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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware. "
2022-02-17 19:50:10 +00:00
}
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-10-20 09:56:10 +00:00
seconds = order . t_to_expire ( Order . Status . TAK )
)
2023-05-08 18:10:37 +00:00
order . save (
update_fields = [
" expires_at " ,
" last_satoshis_time " ,
" taker_bond " ,
" expires_at " ,
]
)
2023-08-06 17:48:20 +00:00
order . log (
f " Taker bond invoice LNPayment( { hold_payment [ ' payment_hash ' ] } , { str ( order . taker_bond ) } ) was created "
)
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 :
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . WFI )
2022-01-18 13:20:19 +00:00
# If status is 'Waiting for invoice' move to Chat
elif order . status == Order . Status . WFE :
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . CHA )
2022-02-17 19:50:10 +00:00
order . expires_at = timezone . now ( ) + timedelta (
2022-10-20 09:56:10 +00:00
seconds = order . t_to_expire ( Order . Status . CHA )
)
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expires_at " ] )
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " fiat_exchange_starts " )
2022-01-12 00:02:17 +00:00
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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Invoice expired. You did not send the escrow in time. "
2022-02-17 19:50:10 +00:00
}
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 :
2023-05-08 18:10:37 +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-10-20 09:56:10 +00:00
escrow_satoshis = cls . escrow_amount ( order , user ) [ 1 ] [
" escrow_amount "
] # Amount was fixed when taker bond was locked, fee applied here
2023-08-06 17:48:20 +00:00
order . log ( f " Escrow invoice amount is calculated as { escrow_satoshis } Sats " )
2023-05-01 10:30:53 +00:00
if user . robot . wants_stealth :
2024-10-16 02:30:27 +00:00
description = f " { config ( " NODE_ALIAS " ) } - Payment reference: { order . reference } . This payment WILL FREEZE IN YOUR WALLET, check on RoboSats if the lock was successful. It will be unlocked (fail) unless you cheat or cancel unilaterally. "
2022-08-12 17:41:06 +00:00
else :
2024-10-16 02:30:27 +00:00
description = f " { config ( " NODE_ALIAS " ) } - 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-10-20 09:56:10 +00:00
cltv_expiry_blocks = cls . compute_cltv_expiry_blocks (
order , " trade_escrow "
) ,
2023-05-22 14:56:15 +00:00
order_id = order . id ,
lnpayment_concept = LNPayment . Concepts . TRESCROW . label ,
time = int ( timezone . now ( ) . timestamp ( ) ) ,
2022-02-17 19:50:10 +00:00
)
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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware. "
2022-02-17 19:50:10 +00:00
}
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
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " trade_escrow " ] )
2023-08-06 17:48:20 +00:00
order . log (
f " Trade escrow invoice LNPayment( { hold_payment [ ' payment_hash ' ] } , { str ( order . trade_escrow ) } ) was created "
)
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-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
2023-05-08 18:10:37 +00:00
order . trade_escrow . save ( update_fields = [ " status " ] )
2023-08-06 17:48:20 +00:00
order . log ( " Trade escrow was <b>settled</b> " )
2022-01-12 00:02:17 +00:00
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-16 21:54:42 +00:00
if LNNode . settle_hold_invoice ( bond . preimage ) :
bond . status = LNPayment . Status . SETLED
2023-05-08 18:10:37 +00:00
bond . save ( update_fields = [ " status " ] )
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
2023-05-08 18:10:37 +00:00
order . trade_escrow . save ( update_fields = [ " status " ] )
2023-08-06 17:48:20 +00:00
order . log ( " Trade escrow was <b>unlocked</b> " )
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
2023-05-08 18:10:37 +00:00
order . trade_escrow . save ( update_fields = [ " status " ] )
2023-08-06 17:48:20 +00:00
order . log ( " Trade escrow was <b>cancelled</b> " )
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-10-20 20:53:51 +00:00
if bond is None :
2022-01-17 18:11:44 +00:00
return
try :
LNNode . cancel_return_hold_invoice ( bond . payment_hash )
2022-01-12 12:57:03 +00:00
bond . status = LNPayment . Status . RETNED
2023-05-08 18:10:37 +00:00
bond . save ( update_fields = [ " status " ] )
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
2023-05-08 18:10:37 +00:00
bond . save ( update_fields = [ " status " ] )
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
2022-06-20 17:56:08 +00:00
def cancel_onchain_payment ( order ) :
2022-10-20 09:56:10 +00:00
""" Cancel onchain_payment if existing """
2022-06-20 17:56:08 +00:00
if order . payout_tx :
order . payout_tx . status = OnchainPayment . Status . CANCE
2023-05-08 18:10:37 +00:00
order . payout_tx . save ( update_fields = [ " status " ] )
2023-08-06 17:48:20 +00:00
order . log (
f " Onchain payment OnchainPayment( { order . payout_tx . id } , { str ( order . payout_tx ) } ) was <b>cancelled</b> "
)
2022-06-20 17:56:08 +00:00
return True
else :
return False
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-10-20 20:53:51 +00:00
if bond is None :
2022-01-17 18:11:44 +00:00
return True
try :
LNNode . cancel_return_hold_invoice ( bond . payment_hash )
bond . status = LNPayment . Status . CANCEL
2023-05-08 18:10:37 +00:00
bond . save ( update_fields = [ " status " ] )
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
2023-05-08 18:10:37 +00:00
bond . save ( update_fields = [ " status " ] )
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
2022-06-16 15:31:30 +00:00
@classmethod
def pay_buyer ( cls , order ) :
2022-10-20 09:56:10 +00:00
""" Pays buyer invoice or onchain address """
2022-06-16 20:01:10 +00:00
2022-06-16 15:31:30 +00:00
# Pay to buyer invoice
if not order . is_swap :
2022-10-20 20:53:51 +00:00
# Background process "follow_invoices" will try to pay this invoice until success
2022-06-16 15:31:30 +00:00
order . payout . status = LNPayment . Status . FLIGHT
2023-05-08 18:10:37 +00:00
order . payout . save ( update_fields = [ " status " ] )
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . PAY )
2024-10-01 19:35:57 +00:00
order . contract_finalization_time = timezone . now ( )
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " contract_finalization_time " ] )
2023-05-08 18:10:37 +00:00
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " trade_successful " )
2023-08-06 17:48:20 +00:00
order . log ( " <b>Paying buyer invoice</b> " )
2022-06-16 15:31:30 +00:00
return True
# Pay onchain to address
else :
2022-06-17 11:36:27 +00:00
if not order . payout_tx . status == OnchainPayment . Status . VALID :
return False
2023-03-14 17:23:11 +00:00
else :
# Add onchain payment to queue
order . payout_tx . status = OnchainPayment . Status . QUEUE
2023-05-08 18:10:37 +00:00
order . payout_tx . save ( update_fields = [ " status " ] )
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . SUC )
2024-10-01 19:35:57 +00:00
order . contract_finalization_time = timezone . now ( )
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " contract_finalization_time " ] )
2023-05-08 18:10:37 +00:00
2024-10-01 19:35:57 +00:00
send_notification . delay ( order_id = order . id , message = " trade_successful " )
2023-08-06 17:48:20 +00:00
order . log ( " <b>Paying buyer onchain address</b> " )
2022-06-16 15:31:30 +00:00
return True
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 .
2023-08-06 17:48:20 +00:00
If User is seller and fiat_sent is true : settle the escrow and pay buyer invoice !
"""
2022-02-17 19:50:10 +00:00
2022-10-20 09:56:10 +00:00
if order . status == Order . Status . CHA or order . status == Order . Status . FSE :
2023-08-06 17:48:20 +00:00
# If buyer mark fiat sent
2022-01-07 18:22:52 +00:00
if cls . is_buyer ( order , user ) :
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . FSE )
2024-10-01 19:35:57 +00:00
order . is_fiat_sent = True
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " is_fiat_sent " ] )
order . log ( " Buyer confirmed ' fiat sent ' " )
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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer. "
2022-02-17 19:50:10 +00:00
}
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
2022-10-20 09:56:10 +00:00
num_satoshis = (
order . payout_tx . num_satoshis
if order . is_swap
else order . payout . num_satoshis
)
2022-06-16 15:31:30 +00:00
if order . trade_escrow . num_satoshis < = num_satoshis :
2022-02-17 19:50:10 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " Woah, something broke badly. Report in the public channels, or open a Github Issue. "
2022-02-17 19:50:10 +00:00
}
2022-10-20 09:56:10 +00:00
2022-06-16 15:31:30 +00:00
# !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
2022-10-20 09:56:10 +00:00
if cls . settle_escrow ( order ) :
2022-01-19 19:37:10 +00:00
order . trade_escrow . status = LNPayment . Status . SETLED
2023-05-08 18:10:37 +00:00
order . trade_escrow . save ( update_fields = [ " status " ] )
2022-02-17 19:50:10 +00:00
2022-01-07 18:22:52 +00:00
# Double check the escrow is settled.
2022-06-16 15:31:30 +00:00
if LNNode . double_check_htlc_is_settled ( order . trade_escrow . payment_hash ) :
2022-10-20 09:56:10 +00:00
# RETURN THE BONDS
2022-01-29 19:51:26 +00:00
cls . return_bond ( order . taker_bond )
cls . return_bond ( order . maker_bond )
2023-08-06 17:48:20 +00:00
order . log ( " Taker bond was <b>unlocked</b> " )
order . log ( " Maker bond was <b>unlocked</b> " )
2022-10-20 20:53:51 +00:00
# !!! KEY LINE - PAYS THE BUYER INVOICE !!!
2022-06-16 15:31:30 +00:00
cls . pay_buyer ( order )
2022-03-05 18:43:15 +00:00
2023-05-08 18:10:37 +00:00
# Computes coordinator trade revenue
cls . compute_proceeds ( order )
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 , {
2022-10-20 09:56:10 +00:00
" bad_request " : " You cannot confirm the fiat payment at this stage "
2022-02-17 19:50:10 +00:00
}
2022-01-07 18:22:52 +00:00
2022-01-12 00:02:17 +00:00
return True , None
2023-04-28 09:19:18 +00:00
@classmethod
def undo_confirm_fiat_sent ( cls , order , user ) :
""" If Order is in the CHAT states:
If user is buyer : fiat_sent goes to true .
"""
if not cls . is_buyer ( order , user ) :
return False , {
" bad_request " : " Only the buyer can undo the fiat sent confirmation. "
}
if order . status != Order . Status . FSE :
return False , {
" bad_request " : " Only orders in Chat and with fiat sent confirmed can be reverted. "
}
2024-10-01 19:35:57 +00:00
order . update_status ( Order . Status . CHA )
2023-04-28 09:19:18 +00:00
order . is_fiat_sent = False
order . reverted_fiat_sent = True
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " is_fiat_sent " , " reverted_fiat_sent " ] )
order . log (
f " Buyer Robot( { user . robot . id } , { user . username } ) reverted the confirmation of ' fiat sent ' "
)
2023-04-28 09:19:18 +00:00
return True , None
2022-10-20 09:56:10 +00:00
def pause_unpause_public_order ( order , user ) :
2022-04-29 18:54:20 +00:00
if not order . maker == user :
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " You cannot pause or unpause an order you did not make "
2022-04-29 18:54:20 +00:00
}
else :
if order . status == Order . Status . PUB :
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . PAU )
order . log (
f " Robot( { user . robot . id } , { user . username } ) paused the public order "
)
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-04-29 18:54:20 +00:00
elif order . status == Order . Status . PAU :
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . PUB )
order . log (
f " Robot( { user . robot . id } , { user . username } ) made public the paused order "
)
2024-07-15 10:33:09 +00:00
nostr_send_order_event . delay ( order_id = order . id )
2022-04-29 18:54:20 +00:00
else :
2023-08-06 17:48:20 +00:00
order . log (
f " Robot( { user . robot . id } , { user . username } ) tried to pause/unpause an order that was not public or paused " ,
level = " WARN " ,
)
2022-04-29 18:54:20 +00:00
return False , {
2022-10-20 09:56:10 +00:00
" bad_request " : " You can only pause/unpause an order that is either public or paused "
2022-04-29 18:54:20 +00:00
}
2023-05-08 18:10:37 +00:00
2022-04-29 18:54:20 +00:00
return True , None
2022-02-04 18:07:09 +00:00
@classmethod
def rate_platform ( cls , user , rating ) :
2023-05-01 10:30:53 +00:00
user . robot . platform_rating = rating
2023-05-08 18:10:37 +00:00
user . robot . save ( update_fields = [ " platform_rating " ] )
2022-02-13 16:43:49 +00:00
return True , None
2022-02-21 23:41:36 +00:00
2022-03-07 21:46:52 +00:00
@classmethod
2023-05-08 18:10:37 +00:00
def add_slashed_rewards ( cls , order , slashed_bond , staked_bond ) :
2022-10-20 09:56:10 +00:00
"""
2022-03-07 21:46:52 +00:00
When a bond is slashed due to overtime , rewards the user that was waiting .
2023-05-05 12:07:13 +00:00
slashed_bond is the bond settled by the robot who forfeits his bond .
staked_bond is the bond that was at stake by the robot who is rewarded .
It may happen that the Sats at stake by the maker are larger than the Sats
at stake by the taker ( range amount orders where the taker does not take the
maximum available ) . In those cases , the change is added back also to the robot
that was slashed ( discounted by the forfeited amount ) .
2022-10-20 09:56:10 +00:00
"""
2023-05-05 12:07:13 +00:00
reward_fraction = config ( " SLASHED_BOND_REWARD_SPLIT " , cast = float , default = 0.5 )
if staked_bond . num_satoshis < slashed_bond . num_satoshis :
slashed_satoshis = min ( slashed_bond . num_satoshis , staked_bond . num_satoshis )
slashed_return = int ( slashed_bond . num_satoshis - slashed_satoshis )
else :
slashed_satoshis = slashed_bond . num_satoshis
slashed_return = 0
reward = int ( slashed_satoshis * reward_fraction )
rewarded_robot = staked_bond . sender . robot
rewarded_robot . earned_rewards + = reward
2023-05-08 18:10:37 +00:00
rewarded_robot . save ( update_fields = [ " earned_rewards " ] )
2023-05-05 12:07:13 +00:00
2023-09-15 09:39:40 +00:00
slashed_robot_log = " "
2023-05-05 12:07:13 +00:00
if slashed_return > 100 :
slashed_robot = slashed_bond . sender . robot
slashed_robot . earned_rewards + = slashed_return
2023-05-08 18:10:37 +00:00
slashed_robot . save ( update_fields = [ " earned_rewards " ] )
2023-09-15 09:39:40 +00:00
slashed_robot_log = " Robot( {slashed_robot.id} , {slashed_robot.user.username} ) was returned {slashed_return} Sats) "
2023-05-08 18:10:37 +00:00
2023-05-16 17:12:15 +00:00
new_proceeds = int ( slashed_satoshis * ( 1 - reward_fraction ) )
order . proceeds + = new_proceeds
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " proceeds " ] )
2023-05-16 17:12:15 +00:00
send_devfund_donation . delay ( order . id , new_proceeds , " slashed bond " )
2023-08-06 17:48:20 +00:00
order . log (
2023-09-15 09:39:40 +00:00
f " Robot( { rewarded_robot . id } , { rewarded_robot . user . username } ) was rewarded { reward } Sats. { slashed_robot_log } "
2023-08-06 17:48:20 +00:00
)
2022-03-07 21:46:52 +00:00
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
2023-05-01 10:30:53 +00:00
if user . robot . earned_rewards < 1 :
2022-03-06 16:08:28 +00:00
return False , { " bad_invoice " : " You have not earned rewards " }
2023-05-01 10:30:53 +00:00
num_satoshis = user . robot . earned_rewards
2022-03-09 11:35:50 +00:00
2023-01-14 12:38:27 +00:00
routing_budget_sats = int (
2022-11-28 16:23:37 +00:00
max (
num_satoshis * float ( config ( " PROPORTIONAL_ROUTING_FEE_LIMIT " ) ) ,
float ( config ( " MIN_FLAT_ROUTING_FEE_LIMIT_REWARD " ) ) ,
)
) # 1000 ppm or 10 sats
2023-05-01 10:30:53 +00:00
routing_budget_ppm = ( routing_budget_sats / float ( num_satoshis ) ) * 1_000_000
2022-11-28 16:23:37 +00:00
reward_payout = LNNode . validate_ln_invoice (
2023-01-14 12:38:27 +00:00
invoice , num_satoshis , routing_budget_ppm
2022-11-28 16:23:37 +00:00
)
2022-03-06 16:08:28 +00:00
if not reward_payout [ " valid " ] :
return False , reward_payout [ " context " ]
2022-03-09 11:35:50 +00:00
try :
lnpayment = LNPayment . objects . create (
2022-10-20 09:56:10 +00:00
concept = LNPayment . Concepts . WITHREWA ,
type = LNPayment . Types . NORM ,
sender = User . objects . get ( username = ESCROW_USERNAME ) ,
status = LNPayment . Status . VALIDI ,
2022-03-09 11:35:50 +00:00
receiver = user ,
2022-10-20 09:56:10 +00:00
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 " ] ,
2022-03-09 11:35:50 +00:00
)
# Might fail if payment_hash already exists in DB
2022-10-20 20:53:51 +00:00
except Exception :
2022-03-09 11:35:50 +00:00
return False , { " bad_invoice " : " Give me a new invoice " }
2022-03-06 16:08:28 +00:00
2023-05-01 10:30:53 +00:00
user . robot . earned_rewards = 0
2023-05-08 18:10:37 +00:00
user . robot . save ( update_fields = [ " earned_rewards " ] )
2022-03-09 11:35:50 +00:00
# Pays the invoice.
paid , failure_reason = LNNode . pay_invoice ( lnpayment )
2022-10-20 09:56:10 +00:00
if paid :
2023-05-01 10:30:53 +00:00
user . robot . earned_rewards = 0
user . robot . claimed_rewards + = num_satoshis
2023-05-08 18:10:37 +00:00
user . robot . save ( update_fields = [ " earned_rewards " , " claimed_rewards " ] )
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.
2022-10-20 09:56:10 +00:00
else :
2023-05-01 10:30:53 +00:00
user . robot . earned_rewards = num_satoshis
2023-05-08 18:10:37 +00:00
user . robot . save ( update_fields = [ " earned_rewards " ] )
2022-03-09 11:35:50 +00:00
context = { }
2022-10-20 09:56:10 +00:00
context [ " bad_invoice " ] = failure_reason
2022-03-09 11:35:50 +00:00
return False , context
2023-05-08 18:10:37 +00:00
@classmethod
def compute_proceeds ( cls , order ) :
"""
Computes Coordinator trade proceeds for finished orders .
"""
if order . is_swap :
2023-05-16 17:12:15 +00:00
payout_sats = (
order . payout_tx . sent_satoshis + order . payout_tx . mining_fee_sats
)
new_proceeds = int ( order . trade_escrow . num_satoshis - payout_sats )
2023-05-08 18:10:37 +00:00
else :
payout_sats = order . payout . num_satoshis + order . payout . fee
2023-05-16 17:12:15 +00:00
new_proceeds = int ( order . trade_escrow . num_satoshis - payout_sats )
2023-05-08 18:10:37 +00:00
2023-05-16 17:12:15 +00:00
order . proceeds + = new_proceeds
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " proceeds " ] )
2023-08-06 17:48:20 +00:00
order . log (
f " Order( { order . id } , { str ( order ) } ) proceedings are incremented by { new_proceeds } Sats, totalling { order . proceeds } Sats "
)
2023-05-16 17:12:15 +00:00
send_devfund_donation . delay ( order . id , new_proceeds , " successful order " )
2023-05-08 18:10:37 +00:00
2022-07-16 11:15:00 +00:00
@classmethod
def summarize_trade ( cls , order , user ) :
2022-10-20 09:56:10 +00:00
"""
2022-07-16 11:15:00 +00:00
Summarizes a finished order . Returns a dict with
amounts , fees , costs , etc , for buyer and seller .
2022-10-20 09:56:10 +00:00
"""
2022-10-20 20:53:51 +00:00
if order . status not in [ Order . Status . SUC , Order . Status . PAY , Order . Status . FAI ] :
2022-10-20 09:56:10 +00:00
return False , { " bad_summary " : " Order has not finished yet " }
2022-07-16 11:15:00 +00:00
context = { }
2022-10-20 09:56:10 +00:00
users = { " taker " : order . taker , " maker " : order . maker }
2022-07-16 11:15:00 +00:00
for order_user in users :
summary = { }
2022-10-20 09:56:10 +00:00
summary [ " trade_fee_percent " ] = (
FEE * MAKER_FEE_SPLIT
if order_user == " maker "
else FEE * ( 1 - MAKER_FEE_SPLIT )
)
summary [ " bond_size_sats " ] = (
order . maker_bond . num_satoshis
if order_user == " maker "
else order . taker_bond . num_satoshis
)
summary [ " bond_size_percent " ] = order . bond_size
summary [ " is_buyer " ] = cls . is_buyer ( order , users [ order_user ] )
2022-07-16 11:15:00 +00:00
2022-10-20 09:56:10 +00:00
if summary [ " is_buyer " ] :
summary [ " sent_fiat " ] = order . amount
2022-07-16 11:15:00 +00:00
if order . is_swap :
2022-10-20 09:56:10 +00:00
summary [ " received_sats " ] = order . payout_tx . sent_satoshis
2022-07-16 11:15:00 +00:00
else :
2022-10-20 09:56:10 +00:00
summary [ " received_sats " ] = order . payout . num_satoshis
2023-05-07 11:11:40 +00:00
summary [ " payment_hash " ] = order . payout . payment_hash
2023-11-16 13:28:53 +00:00
summary [ " preimage " ] = (
order . payout . preimage if order . payout . preimage else " processing "
)
2022-10-20 09:56:10 +00:00
summary [ " trade_fee_sats " ] = round (
2022-11-24 17:42:30 +00:00
order . last_satoshis
- summary [ " received_sats " ]
2022-12-01 18:17:25 +00:00
- ( order . payout . routing_budget_sats if not order . is_swap else 0 )
2022-10-20 09:56:10 +00:00
)
2022-07-16 11:15:00 +00:00
# Only add context for swap costs if the user is the swap recipient. Peer should not know whether it was a swap
if users [ order_user ] == user and order . is_swap :
2022-10-20 09:56:10 +00:00
summary [ " is_swap " ] = order . is_swap
summary [ " received_onchain_sats " ] = order . payout_tx . sent_satoshis
2023-05-07 11:11:40 +00:00
summary [ " address " ] = order . payout_tx . address
summary [ " txid " ] = order . payout_tx . txid
2022-10-20 09:56:10 +00:00
summary [ " mining_fee_sats " ] = order . payout_tx . mining_fee_sats
summary [ " swap_fee_sats " ] = round (
order . payout_tx . num_satoshis
- order . payout_tx . mining_fee_sats
- order . payout_tx . sent_satoshis
)
summary [ " swap_fee_percent " ] = order . payout_tx . swap_fee_rate
summary [ " trade_fee_sats " ] = round (
order . last_satoshis
- summary [ " received_sats " ]
- summary [ " mining_fee_sats " ]
- summary [ " swap_fee_sats " ]
)
2022-07-16 11:15:00 +00:00
else :
2022-10-20 09:56:10 +00:00
summary [ " sent_sats " ] = order . trade_escrow . num_satoshis
summary [ " received_fiat " ] = order . amount
summary [ " trade_fee_sats " ] = round (
summary [ " sent_sats " ] - order . last_satoshis
)
context [ f " { order_user } _summary " ] = summary
2022-07-16 11:15:00 +00:00
platform_summary = { }
2022-10-20 09:56:10 +00:00
platform_summary [ " contract_exchange_rate " ] = float ( order . amount ) / (
2023-05-01 10:30:53 +00:00
float ( order . last_satoshis ) / 100_000_000
2022-10-20 09:56:10 +00:00
)
2022-10-20 20:53:51 +00:00
if order . last_satoshis_time is not None :
2022-10-20 09:56:10 +00:00
platform_summary [ " contract_timestamp " ] = order . last_satoshis_time
2022-11-26 00:06:40 +00:00
if order . contract_finalization_time is None :
order . contract_finalization_time = timezone . now ( )
2023-05-08 18:10:37 +00:00
order . save ( update_fields = [ " contract_finalization_time " ] )
2022-10-20 09:56:10 +00:00
platform_summary [ " contract_total_time " ] = (
order . contract_finalization_time - order . last_satoshis_time
2023-11-16 13:28:53 +00:00
) . total_seconds ( )
2022-07-16 11:15:00 +00:00
if not order . is_swap :
2022-11-24 17:42:30 +00:00
platform_summary [ " routing_budget_sats " ] = order . payout . routing_budget_sats
2022-10-20 09:56:10 +00:00
platform_summary [ " trade_revenue_sats " ] = int (
2023-05-07 11:11:40 +00:00
order . trade_escrow . num_satoshis - order . payout . num_satoshis
2022-10-20 09:56:10 +00:00
)
2022-07-16 11:15:00 +00:00
else :
2022-10-20 09:56:10 +00:00
platform_summary [ " routing_fee_sats " ] = 0
platform_summary [ " trade_revenue_sats " ] = int (
order . trade_escrow . num_satoshis - order . payout_tx . num_satoshis
)
context [ " platform_summary " ] = platform_summary
2022-03-06 16:08:28 +00:00
2022-10-20 09:56:10 +00:00
return True , context