2022-01-17 16:41:55 +00:00
|
|
|
from django.core.management.base import BaseCommand, CommandError
|
2022-01-17 18:11:44 +00:00
|
|
|
|
2022-01-17 16:41:55 +00:00
|
|
|
from api.lightning.node import LNNode
|
2022-01-24 22:53:55 +00:00
|
|
|
from api.tasks import follow_send_payment
|
2022-01-17 23:11:41 +00:00
|
|
|
from api.models import LNPayment, Order
|
|
|
|
from api.logics import Logics
|
|
|
|
|
|
|
|
from django.utils import timezone
|
2022-01-24 22:53:55 +00:00
|
|
|
from datetime import timedelta
|
2022-01-17 16:41:55 +00:00
|
|
|
from decouple import config
|
|
|
|
from base64 import b64decode
|
|
|
|
import time
|
|
|
|
|
|
|
|
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
|
|
|
|
|
|
|
class Command(BaseCommand):
|
|
|
|
|
|
|
|
help = 'Follows all active hold invoices'
|
2022-01-18 13:20:19 +00:00
|
|
|
rest = 5 # seconds between consecutive checks for invoice updates
|
2022-01-17 16:41:55 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
def handle(self, *args, **options):
|
|
|
|
''' Infinite loop to check invoices and retry payments.
|
|
|
|
ever mind database locked error, keep going, print out'''
|
|
|
|
|
|
|
|
while True:
|
|
|
|
time.sleep(self.rest)
|
2022-01-17 16:41:55 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
try:
|
|
|
|
self.follow_hold_invoices()
|
2022-02-12 13:59:59 +00:00
|
|
|
except Exception as e:
|
|
|
|
self.stdout.write(str(e))
|
|
|
|
try:
|
2022-02-04 01:37:24 +00:00
|
|
|
self.send_payments()
|
2022-01-24 22:53:55 +00:00
|
|
|
except Exception as e:
|
|
|
|
self.stdout.write(str(e))
|
2022-01-17 16:41:55 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
def follow_hold_invoices(self):
|
|
|
|
''' Follows and updates LNpayment objects
|
|
|
|
until settled or canceled
|
|
|
|
|
|
|
|
Background: SubscribeInvoices stub iterator would be great to use here.
|
|
|
|
However, it only sends updates when the invoice is OPEN (new) or SETTLED.
|
|
|
|
We are very interested on the other two states (CANCELLED and ACCEPTED).
|
|
|
|
Therefore, this thread (follow_invoices) will iterate over all LNpayment
|
|
|
|
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
|
|
|
|
'''
|
2022-01-19 19:37:10 +00:00
|
|
|
|
2022-01-17 16:41:55 +00:00
|
|
|
lnd_state_to_lnpayment_status = {
|
2022-01-17 23:11:41 +00:00
|
|
|
0: LNPayment.Status.INVGEN, # OPEN
|
|
|
|
1: LNPayment.Status.SETLED, # SETTLED
|
|
|
|
2: LNPayment.Status.CANCEL, # CANCELLED
|
|
|
|
3: LNPayment.Status.LOCKED # ACCEPTED
|
2022-01-17 16:41:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
stub = LNNode.invoicesstub
|
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
# time it for debugging
|
|
|
|
t0 = time.time()
|
|
|
|
queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
|
2022-01-17 16:41:55 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
debug = {}
|
|
|
|
debug['num_active_invoices'] = len(queryset)
|
|
|
|
debug['invoices'] = []
|
|
|
|
at_least_one_changed = False
|
|
|
|
|
|
|
|
for idx, hold_lnpayment in enumerate(queryset):
|
|
|
|
old_status = LNPayment.Status(hold_lnpayment.status).label
|
2022-01-25 15:20:56 +00:00
|
|
|
try:
|
|
|
|
# this is similar to LNNnode.validate_hold_invoice_locked
|
2022-01-24 22:53:55 +00:00
|
|
|
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
|
|
|
|
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
|
|
|
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
|
2022-01-25 15:20:56 +00:00
|
|
|
|
|
|
|
# try saving expiry height
|
|
|
|
if hasattr(response, 'htlcs' ):
|
|
|
|
try:
|
|
|
|
hold_lnpayment.expiry_height = response.htlcs[0].expiry_height
|
|
|
|
except:
|
|
|
|
pass
|
2022-01-17 16:41:55 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
except Exception as e:
|
|
|
|
# If it fails at finding the invoice: it has been canceled.
|
|
|
|
# In RoboSats DB we make a distinction between cancelled and returned (LND does not)
|
|
|
|
if 'unable to locate invoice' in str(e):
|
|
|
|
self.stdout.write(str(e))
|
|
|
|
hold_lnpayment.status = LNPayment.Status.CANCEL
|
2022-01-25 15:20:56 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
# LND restarted.
|
|
|
|
if 'wallet locked, unlock it' in str(e):
|
2022-01-25 15:20:56 +00:00
|
|
|
self.stdout.write(str(timezone.now())+' :: Wallet Locked')
|
2022-01-24 22:53:55 +00:00
|
|
|
# Other write to logs
|
|
|
|
else:
|
|
|
|
self.stdout.write(str(e))
|
|
|
|
|
|
|
|
new_status = LNPayment.Status(hold_lnpayment.status).label
|
2022-01-17 16:41:55 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
# Only save the hold_payments that change (otherwise this function does not scale)
|
|
|
|
changed = not old_status==new_status
|
|
|
|
if changed:
|
|
|
|
# self.handle_status_change(hold_lnpayment, old_status)
|
|
|
|
self.update_order_status(hold_lnpayment)
|
|
|
|
hold_lnpayment.save()
|
|
|
|
|
|
|
|
# Report for debugging
|
2022-01-17 16:41:55 +00:00
|
|
|
new_status = LNPayment.Status(hold_lnpayment.status).label
|
2022-01-24 22:53:55 +00:00
|
|
|
debug['invoices'].append({idx:{
|
|
|
|
'payment_hash': str(hold_lnpayment.payment_hash),
|
|
|
|
'old_status': old_status,
|
|
|
|
'new_status': new_status,
|
|
|
|
}})
|
2022-01-17 16:41:55 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
at_least_one_changed = at_least_one_changed or changed
|
|
|
|
|
|
|
|
debug['time']=time.time()-t0
|
|
|
|
|
|
|
|
if at_least_one_changed:
|
|
|
|
self.stdout.write(str(timezone.now()))
|
|
|
|
self.stdout.write(str(debug))
|
|
|
|
|
2022-02-04 01:37:24 +00:00
|
|
|
def send_payments(self):
|
|
|
|
'''
|
|
|
|
Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts.
|
|
|
|
Checks if any payment is due for retry, and tries to pay it.
|
|
|
|
'''
|
2022-01-17 23:11:41 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
queryset = LNPayment.objects.filter(type=LNPayment.Types.NORM,
|
2022-02-04 01:37:24 +00:00
|
|
|
status=LNPayment.Status.FLIGHT,
|
|
|
|
routing_attempts=0)
|
|
|
|
|
|
|
|
queryset_retries = LNPayment.objects.filter(type=LNPayment.Types.NORM,
|
2022-01-24 22:53:55 +00:00
|
|
|
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
|
2022-02-06 14:50:42 +00:00
|
|
|
routing_attempts__lt=5,
|
2022-01-24 22:53:55 +00:00
|
|
|
last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
|
2022-02-04 01:37:24 +00:00
|
|
|
|
|
|
|
queryset = queryset.union(queryset_retries)
|
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
for lnpayment in queryset:
|
2022-02-04 01:37:24 +00:00
|
|
|
success, _ = follow_send_payment(lnpayment) # Do follow_send_payment.delay() for further concurrency.
|
2022-01-17 23:11:41 +00:00
|
|
|
|
2022-02-04 18:07:09 +00:00
|
|
|
# If failed, reset mision control. (This won't scale well, just a temporary fix)
|
|
|
|
if not success:
|
|
|
|
LNNode.resetmc()
|
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
# If already 3 attempts and last failed. Make it expire (ask for a new invoice) an reset attempts.
|
2022-02-06 14:50:42 +00:00
|
|
|
if not success and lnpayment.routing_attempts > 2:
|
2022-01-24 22:53:55 +00:00
|
|
|
lnpayment.status = LNPayment.Status.EXPIRE
|
|
|
|
lnpayment.routing_attempts = 0
|
|
|
|
lnpayment.save()
|
2022-01-17 23:11:41 +00:00
|
|
|
|
|
|
|
def update_order_status(self, lnpayment):
|
|
|
|
''' Background process following LND hold invoices
|
2022-01-18 13:20:19 +00:00
|
|
|
can catch LNpayments changing status. If they do,
|
2022-01-18 16:57:55 +00:00
|
|
|
the order status might have to change too.'''
|
2022-01-17 23:11:41 +00:00
|
|
|
|
|
|
|
# If the LNPayment goes to LOCKED (ACCEPTED)
|
|
|
|
if lnpayment.status == LNPayment.Status.LOCKED:
|
2022-01-18 16:57:55 +00:00
|
|
|
try:
|
|
|
|
# It is a maker bond => Publish order.
|
2022-01-18 22:17:41 +00:00
|
|
|
if hasattr(lnpayment, 'order_made' ):
|
2022-01-18 16:57:55 +00:00
|
|
|
Logics.publish_order(lnpayment.order_made)
|
|
|
|
return
|
|
|
|
|
|
|
|
# It is a taker bond => close contract.
|
2022-01-18 22:17:41 +00:00
|
|
|
elif hasattr(lnpayment, 'order_taken' ):
|
2022-01-18 16:57:55 +00:00
|
|
|
if lnpayment.order_taken.status == Order.Status.TAK:
|
|
|
|
Logics.finalize_contract(lnpayment.order_taken)
|
|
|
|
return
|
2022-01-17 23:11:41 +00:00
|
|
|
|
2022-01-18 16:57:55 +00:00
|
|
|
# It is a trade escrow => move foward order status.
|
2022-01-18 22:17:41 +00:00
|
|
|
elif hasattr(lnpayment, 'order_escrow' ):
|
2022-01-18 16:57:55 +00:00
|
|
|
Logics.trade_escrow_received(lnpayment.order_escrow)
|
2022-01-18 13:20:19 +00:00
|
|
|
return
|
2022-01-24 22:53:55 +00:00
|
|
|
|
2022-01-18 16:57:55 +00:00
|
|
|
except Exception as e:
|
|
|
|
self.stdout.write(str(e))
|
2022-01-18 13:20:19 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
# If the LNPayment goes to CANCEL from INVGEN, the invoice had expired
|
|
|
|
# If it goes to CANCEL from LOCKED the bond was relased. Order had expired in both cases.
|
|
|
|
# Testing needed for end of time trades!
|
|
|
|
if lnpayment.status == LNPayment.Status.CANCEL :
|
|
|
|
if hasattr(lnpayment, 'order_made' ):
|
|
|
|
Logics.order_expires(lnpayment.order_made)
|
|
|
|
return
|
2022-01-20 20:50:25 +00:00
|
|
|
|
2022-01-24 22:53:55 +00:00
|
|
|
elif hasattr(lnpayment, 'order_taken' ):
|
|
|
|
Logics.order_expires(lnpayment.order_taken)
|
|
|
|
return
|
|
|
|
|
|
|
|
elif hasattr(lnpayment, 'order_escrow' ):
|
|
|
|
Logics.order_expires(lnpayment.order_escrow)
|
|
|
|
return
|
|
|
|
|
|
|
|
# TODO If a lnpayment goes from LOCKED to INVGEN. Totally weird
|
|
|
|
# halt the order
|
|
|
|
if lnpayment.status == LNPayment.Status.INVGEN:
|
|
|
|
pass
|