Work on lightning functionality: locks, settles, cancels, validates. Trading almost working on testnet

This commit is contained in:
Reckless_Satoshi 2022-01-11 16:02:17 -08:00
parent 8bc8f539d0
commit a3375df6e5
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
4 changed files with 154 additions and 129 deletions

View File

@ -19,13 +19,19 @@ MIN_TRADE = 10000
MAX_TRADE = 500000
# Expiration time for HODL invoices and returning collateral in HOURS
BOND_EXPIRY = 8
BOND_EXPIRY = 14
ESCROW_EXPIRY = 8
# Expiration time for locking collateral in MINUTES
# Expiration time for locking collateral in SECONDS
EXP_MAKER_BOND_INVOICE = 300
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
ESCROW_USERNAME = 'admin'

View File

@ -40,23 +40,16 @@ class LNNode():
'''Cancels or returns a hold invoice'''
request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.CancelInvoice(request, metadata=[('macaroon', MACAROON.hex())])
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
if response == None:
return True
else:
return False
return str(response) == "" # True if no response, false otherwise.
@classmethod
def settle_hold_invoice(cls, preimage):
'''settles a hold invoice'''
request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
response = cls.invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
# Fix this: tricky because settling sucessfully an invoice has no response. TODO
if response == None:
return True
else:
return False
# Fix this: tricky because settling sucessfully an invoice has None response. TODO
return str(response)=="" # True if no response, false otherwise.
@classmethod
def gen_hold_invoice(cls, num_satoshis, description, expiry):
@ -90,8 +83,6 @@ class LNNode():
'''Checks if hold invoice is locked'''
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
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.
@classmethod
@ -99,10 +90,10 @@ class LNNode():
'''Checks until hold invoice is locked.
When invoice is locked, returns true.
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.
request = cls.invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
print(invoice)
if timezone.now > expiration:
@ -165,13 +156,9 @@ class LNNode():
payment_request=invoice,
amt_msat=num_satoshis,
fee_limit_sat=fee_limit_sat,
timeout_seconds=60,
)
for response in routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
print(response)
print(response.status)
timeout_seconds=60)
for response in cls.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
if response.status == True:
return True
@ -181,12 +168,10 @@ class LNNode():
def double_check_htlc_is_settled(cls, payment_hash):
''' Just as it sounds. Better safe than sorry!'''
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

View File

@ -19,11 +19,13 @@ MAX_TRADE = int(config('MAX_TRADE'))
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE'))
EXP_TRADE_ESCR_INVOICE = int(config('EXP_TRADE_ESCR_INVOICE'))
BOND_EXPIRY = int(config('BOND_EXPIRY'))
ESCROW_EXPIRY = int(config('ESCROW_EXPIRY'))
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():
@ -53,6 +55,7 @@ class Logics():
else:
order.taker = user
order.status = Order.Status.TAK
order.expires_at = timezone.now() + timedelta(minutes=EXP_TAKER_BOND_INVOICE)
order.save()
return True, None
@ -96,11 +99,29 @@ class Logics():
return price, premium
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.maker = None
order.taker = None
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
def buyer_invoice_amount(cls, order, user):
''' 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']
buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis)
if not buyer_invoice['valid']:
return False, buyer_invoice['context']
@ -144,8 +165,8 @@ class Logics():
'expires_at' : buyer_invoice['expires_at']}
)
# If the order status is 'Waiting for escrow'. Move forward to 'chat'
if order.status == Order.Status.WFE: order.status = Order.Status.CHA
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
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 order.status == Order.Status.WF2:
@ -174,22 +195,6 @@ class Logics():
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):
''' Checks if a user that is not participant of orders
has a limit on taking or making a order'''
@ -212,23 +217,18 @@ class Logics():
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, {}
return True, None
# 2) When maker cancels after bond
'''The order dissapears from book and goes to cancelled.
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)'''
'''The order dissapears from book and goes to cancelled. 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)'''
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):
order.maker_bond.status = LNPayment.Status.SETLED
order.maker_bond.save()
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, {}
return True, None
# 3) When taker cancels before bond
''' The order goes back to the book as public.
@ -242,7 +242,7 @@ class Logics():
order.status = Order.Status.PUB
order.save()
return True, {}
return True, None
# 4) When taker or maker cancel after bond (before escrow)
'''The order goes into cancelled status if maker cancels.
@ -258,7 +258,7 @@ class Logics():
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, {}
return True, None
# 4.b) When taker cancel after bond (before escrow)
'''The order into cancelled status if maker cancels.'''
@ -270,7 +270,7 @@ class Logics():
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.save()
return True, {}
return True, None
# 5) When trade collateral has been posted (after escrow)
'''Always goes to cancelled status. Collaboration is needed.
@ -281,22 +281,34 @@ class Logics():
else:
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
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():
cls.order_expires(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
if order.maker_bond:
cls.check_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:
if cls.is_maker_bond_locked(order):
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)
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}
@classmethod
def check_until_maker_bond_locked(cls, order):
expiration = order.maker_bond.created_at + timedelta(seconds=EXP_MAKER_BOND_INVOICE)
is_locked = LNNode.check_until_invoice_locked(order.payment_hash, expiration)
if is_locked:
order.maker_bond.status = LNPayment.Status.LOCKED
order.maker_bond.save()
order.status = Order.Status.PUB
order.save()
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
def is_taker_bond_locked(cls, order):
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
# (This is the last update to "last_satoshis", it becomes the escrow amount next!)
order.last_satoshis = cls.satoshis_now(order)
order.taker_bond.status = LNPayment.Status.LOCKED
order.taker_bond.save()
# With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(minutes=INVOICE_AND_ESCROW_DURATION)
order.status = Order.Status.WF2
order.save()
return True
return False
@ -348,21 +352,21 @@ class Logics():
@classmethod
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:
# Check if status is INVGEN and still not expired
if order.taker_bond.status == LNPayment.Status.INVGEN:
if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)):
cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
# Return the previous invoice there was with INVGEN status
else:
return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
# Invoice exists, but was already locked or settled
else:
if cls.is_taker_bond_locked(order):
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)
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'],
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()
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
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:
# Check if status is INVGEN and still not expired
if order.trade_escrow.status == LNPayment.Status.INVGEN:
if order.trade_escrow.created_at > (timezone.now()+timedelta(minutes=EXP_TRADE_ESCR_INVOICE)): # Expired
cls.cancel_order(order, user, 4) # State 4, cancel order before trade escrow locked
return False, {'bad_request':'Invoice expired. You did not lock the trade escrow in time.'}
# Return the previous invoice there was with INVGEN status
else:
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
if cls.is_trade_escrow_locked(order):
return False, None
elif order.trade_escrow.status == LNPayment.Status.INVGEN:
return True, {'escrow_invoice':order.trade_escrow.invoice, 'escrow_satoshis':order.trade_escrow.num_satoshis}
escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis)
description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not pay."
# If there was no taker_bond object yet, generates one
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
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
@ -431,37 +450,32 @@ class Logics():
def settle_escrow(order):
''' Settles the trade escrow hold invoice'''
# TODO ERROR HANDLING
valid = LNNode.settle_hold_invoice(order.trade_escrow.preimage)
if valid:
if LNNode.settle_hold_invoice(order.trade_escrow.preimage):
order.trade_escrow.status = LNPayment.Status.SETLED
order.save()
return valid
order.trade_escrow.save()
return True
def settle_maker_bond(order):
''' Settles the maker bond hold invoice'''
# TODO ERROR HANDLING
if LNNode.settle_hold_invoice(order.maker_bond.preimage):
order.maker_bond.status = LNPayment.Status.SETLED
order.save()
order.maker_bond.save()
return True
def settle_taker_bond(order):
''' Settles the taker bond hold invoice'''
# TODO ERROR HANDLING
valid = LNNode.settle_hold_invoice(order.taker_bond.preimage)
if valid:
if LNNode.settle_hold_invoice(order.taker_bond.preimage):
order.taker_bond.status = LNPayment.Status.SETLED
order.save()
return valid
order.taker_bond.save()
return True
def pay_buyer_invoice(order):
''' Pay buyer invoice'''
# TODO ERROR HANDLING
valid = LNNode.pay_invoice(order.buyer_invoice.invoice)
return valid
if LNNode.pay_invoice(order.buyer_invoice.invoice):
return True
@classmethod
def confirm_fiat(cls, order, user):
@ -492,8 +506,28 @@ class Logics():
if cls.pay_buyer_invoice(order): ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
order.status = Order.Status.PAY
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:
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
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

View File

@ -32,7 +32,7 @@ class LNPayment(models.Model):
LOCKED = 1, 'Locked'
SETLED = 2, 'Settled'
RETNED = 3, 'Returned'
MISSNG = 4, 'Missing'
EXPIRE = 4, 'Expired'
VALIDI = 5, 'Valid'
FLIGHT = 6, 'On flight'
FAILRO = 7, 'Routing failed'
@ -44,10 +44,10 @@ class LNPayment(models.Model):
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
# 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)
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))])
created_at = models.DateTimeField()
expires_at = models.DateTimeField()
@ -80,7 +80,7 @@ class Order(models.Model):
DIS = 11, 'In dispute'
CCA = 12, 'Collaboratively cancelled'
PAY = 13, 'Sending satoshis to buyer'
SUC = 14, 'Sucessfully settled'
SUC = 14, 'Sucessful trade'
FAI = 15, 'Failed lightning network routing'
MLD = 16, 'Maker lost dispute'
TLD = 17, 'Taker lost dispute'