mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-31 02:21:35 +00:00
Work on lightning functionality: locks, settles, cancels, validates. Trading almost working on testnet
This commit is contained in:
parent
8bc8f539d0
commit
a3375df6e5
12
.env-sample
12
.env-sample
@ -19,13 +19,19 @@ MIN_TRADE = 10000
|
|||||||
MAX_TRADE = 500000
|
MAX_TRADE = 500000
|
||||||
|
|
||||||
# Expiration time for HODL invoices and returning collateral in HOURS
|
# Expiration time for HODL invoices and returning collateral in HOURS
|
||||||
BOND_EXPIRY = 8
|
BOND_EXPIRY = 14
|
||||||
ESCROW_EXPIRY = 8
|
ESCROW_EXPIRY = 8
|
||||||
|
|
||||||
# Expiration time for locking collateral in MINUTES
|
# Expiration time for locking collateral in SECONDS
|
||||||
EXP_MAKER_BOND_INVOICE = 300
|
EXP_MAKER_BOND_INVOICE = 300
|
||||||
EXP_TAKER_BOND_INVOICE = 200
|
EXP_TAKER_BOND_INVOICE = 200
|
||||||
EXP_TRADE_ESCR_INVOICE = 200
|
|
||||||
|
# Time a order is public in the book HOURS
|
||||||
|
PUBLIC_ORDER_DURATION = 6
|
||||||
|
# Time to provide a valid invoice and the trade escrow MINUTES
|
||||||
|
INVOICE_AND_ESCROW_DURATION = 30
|
||||||
|
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
|
||||||
|
FIAT_EXCHANGE_DURATION = 4
|
||||||
|
|
||||||
# Username for HTLCs escrows
|
# Username for HTLCs escrows
|
||||||
ESCROW_USERNAME = 'admin'
|
ESCROW_USERNAME = 'admin'
|
@ -40,23 +40,16 @@ class LNNode():
|
|||||||
'''Cancels or returns a hold invoice'''
|
'''Cancels or returns a hold invoice'''
|
||||||
request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
|
request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
|
||||||
response = cls.invoicesstub.CancelInvoice(request, metadata=[('macaroon', MACAROON.hex())])
|
response = cls.invoicesstub.CancelInvoice(request, metadata=[('macaroon', MACAROON.hex())])
|
||||||
|
|
||||||
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
|
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
|
||||||
if response == None:
|
return str(response) == "" # True if no response, false otherwise.
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def settle_hold_invoice(cls, preimage):
|
def settle_hold_invoice(cls, preimage):
|
||||||
'''settles a hold invoice'''
|
'''settles a hold invoice'''
|
||||||
request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
|
request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
|
||||||
response = cls.invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
|
response = cls.invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
|
||||||
# Fix this: tricky because settling sucessfully an invoice has no response. TODO
|
# Fix this: tricky because settling sucessfully an invoice has None response. TODO
|
||||||
if response == None:
|
return str(response)=="" # True if no response, false otherwise.
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def gen_hold_invoice(cls, num_satoshis, description, expiry):
|
def gen_hold_invoice(cls, num_satoshis, description, expiry):
|
||||||
@ -90,8 +83,6 @@ class LNNode():
|
|||||||
'''Checks if hold invoice is locked'''
|
'''Checks if hold invoice is locked'''
|
||||||
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
|
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
|
||||||
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||||
print('status here')
|
|
||||||
print(response.state) # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled
|
|
||||||
return response.state == 3 # True if hold invoice is accepted.
|
return response.state == 3 # True if hold invoice is accepted.
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -99,10 +90,10 @@ class LNNode():
|
|||||||
'''Checks until hold invoice is locked.
|
'''Checks until hold invoice is locked.
|
||||||
When invoice is locked, returns true.
|
When invoice is locked, returns true.
|
||||||
If time expires, return False.'''
|
If time expires, return False.'''
|
||||||
# Experimental, needs asyncio
|
# Experimental, might need asyncio. Best if subscribing all invoices and running a background task
|
||||||
# Maybe best to pass LNpayment object and change status live.
|
# Maybe best to pass LNpayment object and change status live.
|
||||||
|
|
||||||
request = cls.invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
|
request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
|
||||||
for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
|
for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
|
||||||
print(invoice)
|
print(invoice)
|
||||||
if timezone.now > expiration:
|
if timezone.now > expiration:
|
||||||
@ -165,13 +156,9 @@ class LNNode():
|
|||||||
payment_request=invoice,
|
payment_request=invoice,
|
||||||
amt_msat=num_satoshis,
|
amt_msat=num_satoshis,
|
||||||
fee_limit_sat=fee_limit_sat,
|
fee_limit_sat=fee_limit_sat,
|
||||||
timeout_seconds=60,
|
timeout_seconds=60)
|
||||||
)
|
|
||||||
|
|
||||||
for response in routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
|
|
||||||
print(response)
|
|
||||||
print(response.status)
|
|
||||||
|
|
||||||
|
for response in cls.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
|
||||||
if response.status == True:
|
if response.status == True:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -181,12 +168,10 @@ class LNNode():
|
|||||||
def double_check_htlc_is_settled(cls, payment_hash):
|
def double_check_htlc_is_settled(cls, payment_hash):
|
||||||
''' Just as it sounds. Better safe than sorry!'''
|
''' Just as it sounds. Better safe than sorry!'''
|
||||||
request = invoicesrpc.LookupInvoiceMsg(payment_hash=payment_hash)
|
request = invoicesrpc.LookupInvoiceMsg(payment_hash=payment_hash)
|
||||||
response = invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||||
|
|
||||||
|
return response.state == 1 # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled/returned
|
||||||
|
|
||||||
if response.state == 'SETTLED':
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
228
api/logics.py
228
api/logics.py
@ -19,11 +19,13 @@ MAX_TRADE = int(config('MAX_TRADE'))
|
|||||||
|
|
||||||
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
|
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
|
||||||
EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE'))
|
EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE'))
|
||||||
EXP_TRADE_ESCR_INVOICE = int(config('EXP_TRADE_ESCR_INVOICE'))
|
|
||||||
|
|
||||||
BOND_EXPIRY = int(config('BOND_EXPIRY'))
|
BOND_EXPIRY = int(config('BOND_EXPIRY'))
|
||||||
ESCROW_EXPIRY = int(config('ESCROW_EXPIRY'))
|
ESCROW_EXPIRY = int(config('ESCROW_EXPIRY'))
|
||||||
|
|
||||||
|
PUBLIC_ORDER_DURATION = int(config('PUBLIC_ORDER_DURATION'))
|
||||||
|
INVOICE_AND_ESCROW_DURATION = int(config('INVOICE_AND_ESCROW_DURATION'))
|
||||||
|
FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
|
||||||
|
|
||||||
class Logics():
|
class Logics():
|
||||||
|
|
||||||
@ -53,6 +55,7 @@ class Logics():
|
|||||||
else:
|
else:
|
||||||
order.taker = user
|
order.taker = user
|
||||||
order.status = Order.Status.TAK
|
order.status = Order.Status.TAK
|
||||||
|
order.expires_at = timezone.now() + timedelta(minutes=EXP_TAKER_BOND_INVOICE)
|
||||||
order.save()
|
order.save()
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@ -96,11 +99,29 @@ class Logics():
|
|||||||
return price, premium
|
return price, premium
|
||||||
|
|
||||||
def order_expires(order):
|
def order_expires(order):
|
||||||
|
''' General case when time runs out. Only
|
||||||
|
used when the maker does not lock a publishing bond'''
|
||||||
order.status = Order.Status.EXP
|
order.status = Order.Status.EXP
|
||||||
order.maker = None
|
order.maker = None
|
||||||
order.taker = None
|
order.taker = None
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
|
def kick_taker(order):
|
||||||
|
''' The taker did not lock the taker_bond. Now he has to go'''
|
||||||
|
# Add a time out to the taker
|
||||||
|
profile = order.taker.profile
|
||||||
|
profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
# Delete the taker_bond payment request, and make order public again
|
||||||
|
if LNNode.cancel_return_hold_invoice(order.taker_bond.payment_hash):
|
||||||
|
order.status = Order.Status.PUB
|
||||||
|
order.taker = None
|
||||||
|
order.taker_bond = None
|
||||||
|
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION) ## TO FIX. Restore the remaining order durantion, not all of it!
|
||||||
|
order.save()
|
||||||
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def buyer_invoice_amount(cls, order, user):
|
def buyer_invoice_amount(cls, order, user):
|
||||||
''' Computes buyer invoice amount. Uses order.last_satoshis,
|
''' Computes buyer invoice amount. Uses order.last_satoshis,
|
||||||
@ -124,7 +145,7 @@ class Logics():
|
|||||||
|
|
||||||
num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount']
|
num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount']
|
||||||
buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
||||||
|
|
||||||
if not buyer_invoice['valid']:
|
if not buyer_invoice['valid']:
|
||||||
return False, buyer_invoice['context']
|
return False, buyer_invoice['context']
|
||||||
|
|
||||||
@ -144,8 +165,8 @@ class Logics():
|
|||||||
'expires_at' : buyer_invoice['expires_at']}
|
'expires_at' : buyer_invoice['expires_at']}
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the order status is 'Waiting for escrow'. Move forward to 'chat'
|
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
|
||||||
if order.status == Order.Status.WFE: order.status = Order.Status.CHA
|
if order.status == Order.Status.WFI: order.status = Order.Status.CHA
|
||||||
|
|
||||||
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
|
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
|
||||||
if order.status == Order.Status.WF2:
|
if order.status == Order.Status.WF2:
|
||||||
@ -174,22 +195,6 @@ class Logics():
|
|||||||
|
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def rate_counterparty(cls, order, user, rating):
|
|
||||||
|
|
||||||
# If the trade is finished
|
|
||||||
if order.status > Order.Status.PAY:
|
|
||||||
# if maker, rates taker
|
|
||||||
if order.maker == user:
|
|
||||||
cls.add_profile_rating(order.taker.profile, rating)
|
|
||||||
# if taker, rates maker
|
|
||||||
if order.taker == user:
|
|
||||||
cls.add_profile_rating(order.maker.profile, rating)
|
|
||||||
else:
|
|
||||||
return False, {'bad_request':'You cannot rate your counterparty yet.'}
|
|
||||||
|
|
||||||
return True, None
|
|
||||||
|
|
||||||
def is_penalized(user):
|
def is_penalized(user):
|
||||||
''' Checks if a user that is not participant of orders
|
''' Checks if a user that is not participant of orders
|
||||||
has a limit on taking or making a order'''
|
has a limit on taking or making a order'''
|
||||||
@ -212,23 +217,18 @@ class Logics():
|
|||||||
order.maker = None
|
order.maker = None
|
||||||
order.status = Order.Status.UCA
|
order.status = Order.Status.UCA
|
||||||
order.save()
|
order.save()
|
||||||
return True, {}
|
return True, None
|
||||||
|
|
||||||
# 2) When maker cancels after bond
|
# 2) When maker cancels after bond
|
||||||
'''The order dissapears from book and goes to cancelled.
|
'''The order dissapears from book and goes to cancelled. Maker is charged the bond to prevent DDOS
|
||||||
Maker is charged the bond to prevent DDOS
|
on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)'''
|
||||||
on the LN node and order book. TODO Only charge a small part
|
|
||||||
of the bond (requires maker submitting an invoice)'''
|
|
||||||
elif order.status == Order.Status.PUB and order.maker == user:
|
elif order.status == Order.Status.PUB and order.maker == user:
|
||||||
#Settle the maker bond (Maker loses the bond for a public order)
|
#Settle the maker bond (Maker loses the bond for cancelling public order)
|
||||||
if cls.settle_maker_bond(order):
|
if cls.settle_maker_bond(order):
|
||||||
order.maker_bond.status = LNPayment.Status.SETLED
|
|
||||||
order.maker_bond.save()
|
|
||||||
|
|
||||||
order.maker = None
|
order.maker = None
|
||||||
order.status = Order.Status.UCA
|
order.status = Order.Status.UCA
|
||||||
order.save()
|
order.save()
|
||||||
return True, {}
|
return True, None
|
||||||
|
|
||||||
# 3) When taker cancels before bond
|
# 3) When taker cancels before bond
|
||||||
''' The order goes back to the book as public.
|
''' The order goes back to the book as public.
|
||||||
@ -242,7 +242,7 @@ class Logics():
|
|||||||
order.status = Order.Status.PUB
|
order.status = Order.Status.PUB
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
return True, {}
|
return True, None
|
||||||
|
|
||||||
# 4) When taker or maker cancel after bond (before escrow)
|
# 4) When taker or maker cancel after bond (before escrow)
|
||||||
'''The order goes into cancelled status if maker cancels.
|
'''The order goes into cancelled status if maker cancels.
|
||||||
@ -258,7 +258,7 @@ class Logics():
|
|||||||
order.maker = None
|
order.maker = None
|
||||||
order.status = Order.Status.UCA
|
order.status = Order.Status.UCA
|
||||||
order.save()
|
order.save()
|
||||||
return True, {}
|
return True, None
|
||||||
|
|
||||||
# 4.b) When taker cancel after bond (before escrow)
|
# 4.b) When taker cancel after bond (before escrow)
|
||||||
'''The order into cancelled status if maker cancels.'''
|
'''The order into cancelled status if maker cancels.'''
|
||||||
@ -270,7 +270,7 @@ class Logics():
|
|||||||
order.status = Order.Status.PUB
|
order.status = Order.Status.PUB
|
||||||
# order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard.
|
# order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard.
|
||||||
order.save()
|
order.save()
|
||||||
return True, {}
|
return True, None
|
||||||
|
|
||||||
# 5) When trade collateral has been posted (after escrow)
|
# 5) When trade collateral has been posted (after escrow)
|
||||||
'''Always goes to cancelled status. Collaboration is needed.
|
'''Always goes to cancelled status. Collaboration is needed.
|
||||||
@ -281,22 +281,34 @@ class Logics():
|
|||||||
else:
|
else:
|
||||||
return False, {'bad_request':'You cannot cancel this order'}
|
return False, {'bad_request':'You cannot cancel this order'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_maker_bond_locked(cls, order):
|
||||||
|
if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
|
||||||
|
order.maker_bond.status = LNPayment.Status.LOCKED
|
||||||
|
order.maker_bond.save()
|
||||||
|
order.status = Order.Status.PUB
|
||||||
|
# With the bond confirmation the order is extended 'public_order_duration' hours
|
||||||
|
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION)
|
||||||
|
order.save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def gen_maker_hold_invoice(cls, order, user):
|
def gen_maker_hold_invoice(cls, order, user):
|
||||||
|
|
||||||
# Do not gen and cancel if order is more than 5 minutes old
|
# Do not gen and cancel if order is older than expiry time
|
||||||
if order.expires_at < timezone.now():
|
if order.expires_at < timezone.now():
|
||||||
cls.order_expires(order)
|
cls.order_expires(order)
|
||||||
return False, {'bad_request':'Invoice expired. You did not confirm publishing the order in time. Make a new order.'}
|
return False, {'bad_request':'Invoice expired. You did not confirm publishing the order in time. Make a new order.'}
|
||||||
|
|
||||||
# Return the previous invoice if there was one and is still unpaid
|
# Return the previous invoice if there was one and is still unpaid
|
||||||
if order.maker_bond:
|
if order.maker_bond:
|
||||||
cls.check_maker_bond_locked(order)
|
if cls.is_maker_bond_locked(order):
|
||||||
if order.maker_bond.status == LNPayment.Status.INVGEN:
|
|
||||||
return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis}
|
|
||||||
else:
|
|
||||||
return False, None
|
return False, None
|
||||||
|
elif order.maker_bond.status == LNPayment.Status.INVGEN:
|
||||||
|
return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis}
|
||||||
|
|
||||||
|
# If there was no maker_bond object yet, generates one
|
||||||
order.last_satoshis = cls.satoshis_now(order)
|
order.last_satoshis = cls.satoshis_now(order)
|
||||||
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||||
|
|
||||||
@ -323,24 +335,16 @@ class Logics():
|
|||||||
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
|
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_until_maker_bond_locked(cls, order):
|
def is_taker_bond_locked(cls, order):
|
||||||
expiration = order.maker_bond.created_at + timedelta(seconds=EXP_MAKER_BOND_INVOICE)
|
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
|
||||||
is_locked = LNNode.check_until_invoice_locked(order.payment_hash, expiration)
|
# 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!)
|
||||||
if is_locked:
|
order.last_satoshis = cls.satoshis_now(order)
|
||||||
order.maker_bond.status = LNPayment.Status.LOCKED
|
order.taker_bond.status = LNPayment.Status.LOCKED
|
||||||
order.maker_bond.save()
|
order.taker_bond.save()
|
||||||
order.status = Order.Status.PUB
|
# With the bond confirmation the order is extended 'public_order_duration' hours
|
||||||
|
order.expires_at = timezone.now() + timedelta(minutes=INVOICE_AND_ESCROW_DURATION)
|
||||||
order.save()
|
order.status = Order.Status.WF2
|
||||||
return is_locked
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def check_maker_bond_locked(cls, order):
|
|
||||||
if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
|
|
||||||
order.maker_bond.status = LNPayment.Status.LOCKED
|
|
||||||
order.maker_bond.save()
|
|
||||||
order.status = Order.Status.PUB
|
|
||||||
order.save()
|
order.save()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -348,21 +352,21 @@ class Logics():
|
|||||||
@classmethod
|
@classmethod
|
||||||
def gen_taker_hold_invoice(cls, order, user):
|
def gen_taker_hold_invoice(cls, order, user):
|
||||||
|
|
||||||
# Do not gen and cancel if a taker invoice is there and older than X minutes and unpaid still
|
# Do not gen and kick out the taker if order is older than expiry time
|
||||||
|
if order.expires_at < timezone.now():
|
||||||
|
cls.kick_taker(order)
|
||||||
|
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
|
||||||
|
|
||||||
|
# Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting.
|
||||||
if order.taker_bond:
|
if order.taker_bond:
|
||||||
# Check if status is INVGEN and still not expired
|
# Check if status is INVGEN and still not expired
|
||||||
if order.taker_bond.status == LNPayment.Status.INVGEN:
|
if cls.is_taker_bond_locked(order):
|
||||||
if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)):
|
|
||||||
cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond
|
|
||||||
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
|
|
||||||
# Return the previous invoice there was with INVGEN status
|
|
||||||
else:
|
|
||||||
return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
|
|
||||||
# Invoice exists, but was already locked or settled
|
|
||||||
else:
|
|
||||||
return False, None
|
return False, None
|
||||||
|
elif order.taker_bond.status == LNPayment.Status.INVGEN:
|
||||||
|
return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
|
||||||
|
|
||||||
order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE
|
# If there was no taker_bond object yet, generates one
|
||||||
|
order.last_satoshis = cls.satoshis_now(order)
|
||||||
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||||
description = f"RoboSats - Taking '{str(order)}' - This is a taker bond, it will freeze in your wallet. It automatically returns. It will be charged if you cheat or cancel."
|
description = f"RoboSats - Taking '{str(order)}' - This is a taker bond, it will freeze in your wallet. It automatically returns. It will be charged if you cheat or cancel."
|
||||||
|
|
||||||
@ -383,30 +387,45 @@ class Logics():
|
|||||||
created_at = hold_payment['created_at'],
|
created_at = hold_payment['created_at'],
|
||||||
expires_at = hold_payment['expires_at'])
|
expires_at = hold_payment['expires_at'])
|
||||||
|
|
||||||
# Extend expiry time to allow for escrow deposit
|
|
||||||
## Not here, on func for confirming taker collar. order.expires_at = timezone.now() + timedelta(minutes=EXP_TRADE_ESCR_INVOICE)
|
|
||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
|
return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_trade_escrow_locked(cls, order):
|
||||||
|
if LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
|
||||||
|
order.trade_escrow.status = LNPayment.Status.LOCKED
|
||||||
|
order.trade_escrow.save()
|
||||||
|
# If status is 'Waiting for both' move to Waiting for invoice
|
||||||
|
if order.status == Order.Status.WF2:
|
||||||
|
order.status = Order.Status.WFI
|
||||||
|
# If status is 'Waiting for invoice' move to Chat
|
||||||
|
elif order.status == Order.Status.WFI:
|
||||||
|
order.status = Order.Status.CHA
|
||||||
|
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION)
|
||||||
|
order.save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def gen_escrow_hold_invoice(cls, order, user):
|
def gen_escrow_hold_invoice(cls, order, user):
|
||||||
# Do not generate and cancel if an invoice is there and older than X minutes and unpaid still
|
|
||||||
|
# Do not generate if escrow deposit time has expired
|
||||||
|
if order.expires_at < timezone.now():
|
||||||
|
cls.cancel_order(order,user)
|
||||||
|
return False, {'bad_request':'Invoice expired. You did not send the escrow in time.'}
|
||||||
|
|
||||||
|
# Do not gen if an escrow invoice exist. Do not return if it is already locked. Return the old one if still waiting.
|
||||||
if order.trade_escrow:
|
if order.trade_escrow:
|
||||||
# Check if status is INVGEN and still not expired
|
# Check if status is INVGEN and still not expired
|
||||||
if order.trade_escrow.status == LNPayment.Status.INVGEN:
|
if cls.is_trade_escrow_locked(order):
|
||||||
if order.trade_escrow.created_at > (timezone.now()+timedelta(minutes=EXP_TRADE_ESCR_INVOICE)): # Expired
|
return False, None
|
||||||
cls.cancel_order(order, user, 4) # State 4, cancel order before trade escrow locked
|
elif order.trade_escrow.status == LNPayment.Status.INVGEN:
|
||||||
return False, {'bad_request':'Invoice expired. You did not lock the trade escrow in time.'}
|
return True, {'escrow_invoice':order.trade_escrow.invoice, 'escrow_satoshis':order.trade_escrow.num_satoshis}
|
||||||
# Return the previous invoice there was with INVGEN status
|
|
||||||
else:
|
|
||||||
return True, {'escrow_invoice': order.trade_escrow.invoice, 'escrow_satoshis':order.trade_escrow.num_satoshis}
|
|
||||||
# Invoice exists, but was already locked or settled
|
|
||||||
else:
|
|
||||||
return False, None # Does not return any context of a healthy locked escrow
|
|
||||||
|
|
||||||
escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis)
|
# If there was no taker_bond object yet, generates one
|
||||||
description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not pay."
|
escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked
|
||||||
|
description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
|
||||||
|
|
||||||
# Gen hold Invoice
|
# Gen hold Invoice
|
||||||
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
|
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
|
||||||
@ -431,37 +450,32 @@ class Logics():
|
|||||||
def settle_escrow(order):
|
def settle_escrow(order):
|
||||||
''' Settles the trade escrow hold invoice'''
|
''' Settles the trade escrow hold invoice'''
|
||||||
# TODO ERROR HANDLING
|
# TODO ERROR HANDLING
|
||||||
valid = LNNode.settle_hold_invoice(order.trade_escrow.preimage)
|
if LNNode.settle_hold_invoice(order.trade_escrow.preimage):
|
||||||
if valid:
|
|
||||||
order.trade_escrow.status = LNPayment.Status.SETLED
|
order.trade_escrow.status = LNPayment.Status.SETLED
|
||||||
order.save()
|
order.trade_escrow.save()
|
||||||
|
return True
|
||||||
return valid
|
|
||||||
|
|
||||||
def settle_maker_bond(order):
|
def settle_maker_bond(order):
|
||||||
''' Settles the maker bond hold invoice'''
|
''' Settles the maker bond hold invoice'''
|
||||||
# TODO ERROR HANDLING
|
# TODO ERROR HANDLING
|
||||||
if LNNode.settle_hold_invoice(order.maker_bond.preimage):
|
if LNNode.settle_hold_invoice(order.maker_bond.preimage):
|
||||||
order.maker_bond.status = LNPayment.Status.SETLED
|
order.maker_bond.status = LNPayment.Status.SETLED
|
||||||
order.save()
|
order.maker_bond.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def settle_taker_bond(order):
|
def settle_taker_bond(order):
|
||||||
''' Settles the taker bond hold invoice'''
|
''' Settles the taker bond hold invoice'''
|
||||||
# TODO ERROR HANDLING
|
# TODO ERROR HANDLING
|
||||||
valid = LNNode.settle_hold_invoice(order.taker_bond.preimage)
|
if LNNode.settle_hold_invoice(order.taker_bond.preimage):
|
||||||
if valid:
|
|
||||||
order.taker_bond.status = LNPayment.Status.SETLED
|
order.taker_bond.status = LNPayment.Status.SETLED
|
||||||
order.save()
|
order.taker_bond.save()
|
||||||
|
return True
|
||||||
return valid
|
|
||||||
|
|
||||||
def pay_buyer_invoice(order):
|
def pay_buyer_invoice(order):
|
||||||
''' Pay buyer invoice'''
|
''' Pay buyer invoice'''
|
||||||
# TODO ERROR HANDLING
|
# TODO ERROR HANDLING
|
||||||
|
if LNNode.pay_invoice(order.buyer_invoice.invoice):
|
||||||
valid = LNNode.pay_invoice(order.buyer_invoice.invoice)
|
return True
|
||||||
return valid
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def confirm_fiat(cls, order, user):
|
def confirm_fiat(cls, order, user):
|
||||||
@ -492,8 +506,28 @@ class Logics():
|
|||||||
if cls.pay_buyer_invoice(order): ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
if cls.pay_buyer_invoice(order): ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
||||||
order.status = Order.Status.PAY
|
order.status = Order.Status.PAY
|
||||||
order.buyer_invoice.status = LNPayment.Status.PAYING
|
order.buyer_invoice.status = LNPayment.Status.PAYING
|
||||||
|
|
||||||
|
# RETURN THE BONDS
|
||||||
|
LNNode.cancel_return_hold_invoice(order.taker_bond.payment_hash)
|
||||||
|
LNNode.cancel_return_hold_invoice(order.maker_bond.payment_hash)
|
||||||
else:
|
else:
|
||||||
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
|
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
|
||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def rate_counterparty(cls, order, user, rating):
|
||||||
|
|
||||||
|
# If the trade is finished
|
||||||
|
if order.status > Order.Status.PAY:
|
||||||
|
# if maker, rates taker
|
||||||
|
if order.maker == user:
|
||||||
|
cls.add_profile_rating(order.taker.profile, rating)
|
||||||
|
# if taker, rates maker
|
||||||
|
if order.taker == user:
|
||||||
|
cls.add_profile_rating(order.maker.profile, rating)
|
||||||
|
else:
|
||||||
|
return False, {'bad_request':'You cannot rate your counterparty yet.'}
|
||||||
|
|
||||||
return True, None
|
return True, None
|
@ -32,7 +32,7 @@ class LNPayment(models.Model):
|
|||||||
LOCKED = 1, 'Locked'
|
LOCKED = 1, 'Locked'
|
||||||
SETLED = 2, 'Settled'
|
SETLED = 2, 'Settled'
|
||||||
RETNED = 3, 'Returned'
|
RETNED = 3, 'Returned'
|
||||||
MISSNG = 4, 'Missing'
|
EXPIRE = 4, 'Expired'
|
||||||
VALIDI = 5, 'Valid'
|
VALIDI = 5, 'Valid'
|
||||||
FLIGHT = 6, 'On flight'
|
FLIGHT = 6, 'On flight'
|
||||||
FAILRO = 7, 'Routing failed'
|
FAILRO = 7, 'Routing failed'
|
||||||
@ -44,10 +44,10 @@ class LNPayment(models.Model):
|
|||||||
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
|
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
|
||||||
|
|
||||||
# payment info
|
# payment info
|
||||||
invoice = models.CharField(max_length=500, unique=True, null=True, default=None, blank=True)
|
invoice = models.CharField(max_length=1000, unique=True, null=True, default=None, blank=True)
|
||||||
payment_hash = models.CharField(max_length=100, unique=True, null=True, default=None, blank=True)
|
payment_hash = models.CharField(max_length=100, unique=True, null=True, default=None, blank=True)
|
||||||
preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True)
|
preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True)
|
||||||
description = models.CharField(max_length=150, unique=False, null=True, default=None, blank=True)
|
description = models.CharField(max_length=200, unique=False, null=True, default=None, blank=True)
|
||||||
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
|
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
|
||||||
created_at = models.DateTimeField()
|
created_at = models.DateTimeField()
|
||||||
expires_at = models.DateTimeField()
|
expires_at = models.DateTimeField()
|
||||||
@ -80,7 +80,7 @@ class Order(models.Model):
|
|||||||
DIS = 11, 'In dispute'
|
DIS = 11, 'In dispute'
|
||||||
CCA = 12, 'Collaboratively cancelled'
|
CCA = 12, 'Collaboratively cancelled'
|
||||||
PAY = 13, 'Sending satoshis to buyer'
|
PAY = 13, 'Sending satoshis to buyer'
|
||||||
SUC = 14, 'Sucessfully settled'
|
SUC = 14, 'Sucessful trade'
|
||||||
FAI = 15, 'Failed lightning network routing'
|
FAI = 15, 'Failed lightning network routing'
|
||||||
MLD = 16, 'Maker lost dispute'
|
MLD = 16, 'Maker lost dispute'
|
||||||
TLD = 17, 'Taker lost dispute'
|
TLD = 17, 'Taker lost dispute'
|
||||||
|
Loading…
Reference in New Issue
Block a user