lndg/jobs.py

853 lines
No EOL
52 KiB
Python

import django
import random
from time import sleep
from django.db.models import Max, Sum, Avg, Count
from django.db.models.functions import TruncDay
from datetime import datetime, timedelta
from gui.lnd_deps import lightning_pb2 as ln
from gui.lnd_deps import lightning_pb2_grpc as lnrpc
from gui.lnd_deps import signer_pb2 as lns
from gui.lnd_deps import signer_pb2_grpc as lnsigner
from gui.lnd_deps.lnd_connect import lnd_connect
from lndg import settings
from os import environ
from requests import get
environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings'
django.setup()
from gui.models import Payments, PaymentHops, Invoices, Forwards, Channels, Peers, Onchain, Closures, Resolutions, PendingHTLCs, LocalSettings, FailedHTLCs, Autofees, InboundFeeLog, PendingChannels, HistFailedHTLC, PeerEvents
import af
import logging
logger = logging.getLogger('[Data]')
_SELF_PUBKEY = None
def _get_self_pubkey(stub):
global _SELF_PUBKEY
if _SELF_PUBKEY is None:
try:
_SELF_PUBKEY = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey
except Exception as e:
logger.critical(f'GetInfo failed to get self pubkey: {e}')
raise
return _SELF_PUBKEY
def update_payments(stub):
# Detect if index out of sync and resync if required
last_payment_index = stub.ListPayments(ln.ListPaymentsRequest(include_incomplete=True, reversed=True, max_payments=1)).last_index_offset
last_db_payment_index = Payments.objects.aggregate(Max('index'))['index__max'] or 0
if last_payment_index != 0 and last_db_payment_index > last_payment_index:
logger.warning(f'Payment data index greater than LND index, payment reindexing triggered')
Payments.objects.all().update(index=0)
page_size = 300
index_offset = 0
while True:
resp = stub.ListPayments(ln.ListPaymentsRequest(
include_incomplete=True,
index_offset=index_offset,
max_payments=page_size
))
payments = resp.payments
for p in payments:
try:
db_payment = Payments.objects.get(payment_hash=p.payment_hash)
db_payment.index = p.payment_index
db_payment.save()
except Payments.DoesNotExist:
logger.warning(f'Payment not found during reindex: {p.payment_hash}')
index_offset = resp.last_index_offset
if len(payments) < page_size:
break
# Update inflight payments
self_pubkey = _get_self_pubkey(stub)
inflight_payments = Payments.objects.filter(status=1).order_by('index')
for payment in inflight_payments:
payment_data = stub.ListPayments(ln.ListPaymentsRequest(include_incomplete=True, index_offset=payment.index-1, max_payments=1)).payments
# Ignore inflight payments before 30 days
if len(payment_data) > 0 and payment.payment_hash == payment_data[0].payment_hash and payment.creation_date > (datetime.now() - timedelta(days=30)):
update_payment(stub, payment_data[0], self_pubkey)
else:
payment.status = 3
payment.save()
# Bulk payment sync
page_size = 300
index_offset = last_db_payment_index
while True:
resp = stub.ListPayments(ln.ListPaymentsRequest(
include_incomplete=True,
index_offset=index_offset,
max_payments=page_size
))
payments = resp.payments
if not payments:
break
page_hashes = [p.payment_hash for p in payments]
existing_hashes = set(list(
Payments.objects.filter(payment_hash__in=page_hashes)
.only('payment_hash').values_list('payment_hash', flat=True)
))
new_hashes = set(page_hashes) - existing_hashes
new_payments = []
for p in payments:
if p.payment_hash in new_hashes:
new_payments.append(Payments(
creation_date=datetime.fromtimestamp(p.creation_date),
payment_hash=p.payment_hash,
value=round(p.value_msat/1000, 3),
fee=round(p.fee_msat/1000, 3),
status=p.status,
index=p.payment_index
))
if new_payments:
try:
Payments.objects.bulk_create(new_payments, ignore_conflicts=True, batch_size=page_size)
except Exception as e:
logger.error(f'Error bulk inserting payments: {str(e)}')
for p in payments:
if (p.payment_hash in new_hashes) or (p.status in (0, 1)):
update_payment(stub, p, self_pubkey)
index_offset = resp.last_index_offset
if len(payments) < page_size:
break
def update_payment(stub, payment, self_pubkey):
db_payment = Payments.objects.get(payment_hash=payment.payment_hash)
db_payment.creation_date = datetime.fromtimestamp(payment.creation_date)
db_payment.value = round(payment.value_msat/1000, 3)
db_payment.fee = round(payment.fee_msat/1000, 3)
db_payment.status = payment.status
db_payment.index = payment.payment_index
if payment.status == 2 or payment.status == 1:
PaymentHops.objects.filter(payment_hash=db_payment).delete()
db_payment.chan_out = None
db_payment.rebal_chan = None
db_payment.save()
for attempt in payment.htlcs:
if attempt.status == 1 or attempt.status == 0:
hops = attempt.route.hops
hop_count = 0
cost_to = 0
total_hops = len(hops)
for hop in hops:
hop_count += 1
try:
alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=hop.pub_key, include_channels=False)).node.alias
except:
alias = ''
fee = hop.fee_msat/1000
if hop_count == total_hops:
# Add additional HTLC information in last hop alias
alias += f'[ {payment.status}-{attempt.status}-{attempt.failure.code}-{attempt.failure.failure_source_index} ]'
if attempt.status == 1 or attempt.status == 0 or (attempt.status == 2 and attempt.failure.code in (1,2,12)):
PaymentHops(payment_hash=db_payment, attempt_id=attempt.attempt_id, step=hop_count, chan_id=hop.chan_id, alias=alias, chan_capacity=hop.chan_capacity, node_pubkey=hop.pub_key, amt=round(hop.amt_to_forward_msat/1000, 3), fee=round(fee, 3), cost_to=round(cost_to, 3)).save()
cost_to += fee
if hop_count == 1 and attempt.status == 1:
if db_payment.chan_out is None:
db_payment.chan_out = hop.chan_id
db_payment.chan_out_alias = alias
else:
db_payment.chan_out = 'MPP'
db_payment.chan_out_alias = 'MPP'
if hop_count == total_hops and 5482373484 in hop.custom_records and db_payment.keysend_preimage is None:
records = hop.custom_records
message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None
db_payment.keysend_preimage = records[5482373484].hex()
db_payment.message = message
if hop_count == total_hops and hop.pub_key == self_pubkey and db_payment.rebal_chan is None:
db_payment.rebal_chan = hop.chan_id
db_payment.save()
def update_invoices(stub):
# Detect if index out of sync and resync if required
last_invoice_index = stub.ListInvoices(ln.ListInvoiceRequest(reversed=True, num_max_invoices=1)).last_index_offset
last_db_inv_index = Invoices.objects.filter(state=1).aggregate(Max('index'))['index__max'] or 0
if last_invoice_index != 0 and last_db_inv_index > last_invoice_index:
logger.warning(f'Invoice data index greater than LND index, invoice reindexing triggered')
Invoices.objects.all().update(index=0)
page_size = 300
index_offset = 0
while True:
resp = stub.ListInvoices(ln.ListInvoiceRequest(
index_offset=index_offset,
num_max_invoices=page_size
))
invoices = resp.invoices
for i in invoices:
try:
db_invoice = Invoices.objects.get(r_hash=i.r_hash.hex())
db_invoice.index = i.add_index
db_invoice.save()
except Invoices.DoesNotExist:
logger.warning(f'Invoice not found during reindex: {i.r_hash.hex()}')
index_offset = resp.last_index_offset
if len(invoices) < page_size:
break
Invoices.objects.filter(state=0, index=0).update(state=2)
# Refresh all currently open invoices
open_invoices = Invoices.objects.filter(state=0).order_by('index')
for open_invoice in open_invoices:
invoice_data = stub.ListInvoices(ln.ListInvoiceRequest(index_offset=open_invoice.index-1, num_max_invoices=1)).invoices
if len(invoice_data) > 0 and open_invoice.r_hash == invoice_data[0].r_hash.hex():
update_invoice(stub, invoice_data[0], open_invoice)
else:
open_invoice.state = 2
open_invoice.save()
# Bulk invoice sync
index_offset = Invoices.objects.aggregate(Max('index'))['index__max'] or 0
page_size = 300
while True:
resp = stub.ListInvoices(ln.ListInvoiceRequest(
index_offset=index_offset,
num_max_invoices=page_size
))
if not resp.invoices:
break
new_batch = []
to_update = []
for inv in resp.invoices:
db_inv = Invoices(
creation_date=datetime.fromtimestamp(inv.creation_date),
r_hash=inv.r_hash.hex(),
value=round(inv.value_msat / 1000, 3),
amt_paid=inv.amt_paid_sat,
state=inv.state,
index=inv.add_index,
)
new_batch.append(db_inv)
if inv.state == 1:
to_update.append((inv, db_inv))
if new_batch:
try:
Invoices.objects.bulk_create(new_batch, batch_size=page_size, ignore_conflicts=True)
except Exception as e:
logger.error(f'Error bulk inserting invoices: {str(e)}')
for inv, db_inv in to_update:
update_invoice(stub, inv, db_inv)
index_offset = resp.last_index_offset
if len(resp.invoices) < page_size:
break
def update_invoice(stub, invoice, db_invoice):
if invoice.state == 1:
if len(invoice.htlcs) > 0:
chan_in_id = invoice.htlcs[0].chan_id
alias = Channels.objects.filter(chan_id=chan_in_id)[0].alias if Channels.objects.filter(chan_id=chan_in_id).exists() else None
records = invoice.htlcs[0].custom_records
keysend_preimage = records[5482373484].hex() if 5482373484 in records else None
message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None
if 34349337 in records and 34349339 in records and 34349343 in records and 34349334 in records:
signerstub = lnsigner.SignerStub(lnd_connect())
self_pubkey = _get_self_pubkey(stub)
try:
valid = signerstub.VerifyMessage(lns.VerifyMessageReq(msg=(records[34349339]+bytes.fromhex(self_pubkey)+records[34349343]+records[34349334]), signature=records[34349337], pubkey=records[34349339])).valid
except:
logger.error(f'Unable to validate signature on invoice: {invoice.r_hash.hex()}')
valid = False
sender = records[34349339].hex() if valid == True else None
try:
sender_alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=sender, include_channels=False)).node.alias if sender != None else None
except:
sender_alias = None
else:
sender = None
sender_alias = None
else:
chan_in_id = None
alias = None
keysend_preimage = None
message = None
sender = None
sender_alias = None
db_invoice.state = invoice.state
db_invoice.amt_paid = invoice.amt_paid_sat
db_invoice.settle_date = datetime.fromtimestamp(invoice.settle_date)
db_invoice.chan_in = chan_in_id
db_invoice.chan_in_alias = alias
db_invoice.keysend_preimage = keysend_preimage
db_invoice.message = message
db_invoice.sender = sender
db_invoice.sender_alias = sender_alias
else:
db_invoice.state = invoice.state
db_invoice.save()
def update_forwards(stub):
latest_forward = Forwards.objects.order_by('-forward_date', '-id').first()
start_time = int(latest_forward.forward_date.timestamp()) if latest_forward else 1420070400
processed_count = Forwards.objects.filter(forward_date=latest_forward.forward_date).count() if latest_forward else 0
response = stub.ForwardingHistory(ln.ForwardingHistoryRequest(
start_time=start_time,
index_offset=processed_count,
num_max_events=1000
))
forwards = response.forwarding_events
if not forwards:
return
new_forwards = []
for forward in forwards:
inbound_channel = Channels.objects.get(chan_id=forward.chan_id_in) if Channels.objects.filter(chan_id=forward.chan_id_in).exists() else None
outbound_channel = Channels.objects.get(chan_id=forward.chan_id_out) if Channels.objects.filter(chan_id=forward.chan_id_out).exists() else None
forward_datetime = datetime.fromtimestamp(forward.timestamp)
amt_in_msat = forward.amt_in_msat
amt_out_msat = forward.amt_out_msat
in_fee_msat = 0
if outbound_channel and outbound_channel.fees_updated < forward_datetime:
out_fee_msat = int((amt_out_msat * (outbound_channel.local_fee_rate / 1000000)) + outbound_channel.local_base_fee)
if forward.fee_msat < out_fee_msat:
in_fee_msat = out_fee_msat - forward.fee_msat
incoming_peer_alias = (inbound_channel.remote_pubkey[:12] if inbound_channel.alias == '' else inbound_channel.alias) if inbound_channel else forward.peer_alias_in
outgoing_peer_alias = (outbound_channel.remote_pubkey[:12] if outbound_channel.alias == '' else outbound_channel.alias) if outbound_channel else forward.peer_alias_out
new_forwards.append(Forwards(
forward_date=forward_datetime,
chan_id_in=forward.chan_id_in,
chan_id_out=forward.chan_id_out,
chan_in_alias=incoming_peer_alias,
chan_out_alias=outgoing_peer_alias,
amt_in_msat=amt_in_msat,
amt_out_msat=amt_out_msat,
fee=round(forward.fee_msat / 1000, 3),
inbound_fee=round(in_fee_msat / 1000, 3)
))
Forwards.objects.bulk_create(new_forwards)
def disconnectpeer(stub, peer):
try:
stub.DisconnectPeer(ln.DisconnectPeerRequest(pub_key=peer.pubkey))
logger.info(f'Disconnected peer {peer.alias} {peer.pubkey}')
peer.connected = False
peer.save()
except Exception as e:
logger.error(f'Error disconnecting peer {peer.alias} {peer.pubkey}: {str(e)}')
def update_channels(stub):
counter = 0
chan_list = []
channels = stub.ListChannels(ln.ListChannelsRequest()).channels
PendingHTLCs.objects.all().delete()
get_info = stub.GetInfo(ln.GetInfoRequest())
block_height = get_info.block_height
version = get_info.version
for channel in channels:
if Channels.objects.filter(chan_id=channel.chan_id).exists():
# Update the channel record with the most current data
db_channel = Channels.objects.filter(chan_id=channel.chan_id)[0]
pending_channel = None
else:
# Create a record for this new channel
try:
alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=channel.remote_pubkey, include_channels=False)).node.alias
except:
alias = ''
channel_point = channel.channel_point
txid, index = channel_point.split(':')
db_channel = Channels()
db_channel.remote_pubkey = channel.remote_pubkey
db_channel.chan_id = channel.chan_id
db_channel.short_chan_id = str(channel.chan_id >> 40) + 'x' + str(channel.chan_id >> 16 & 0xFFFFFF) + 'x' + str(channel.chan_id & 0xFFFF)
db_channel.initiator = channel.initiator
db_channel.alias = alias
db_channel.funding_txid = txid
db_channel.output_index = index
db_channel.capacity = channel.capacity
db_channel.private = channel.private
db_channel.push_amt = channel.push_amount_sat
db_channel.close_address = channel.close_address
pending_channel = PendingChannels.objects.filter(funding_txid=txid, output_index=index)[0] if PendingChannels.objects.filter(funding_txid=txid, output_index=index).exists() else None
# Update basic channel data
db_channel.local_balance = channel.local_balance
db_channel.remote_balance = channel.remote_balance
db_channel.unsettled_balance = channel.unsettled_balance
db_channel.local_commit = channel.commit_fee
db_channel.local_chan_reserve = channel.local_chan_reserve_sat
db_channel.num_updates = channel.num_updates
db_channel.is_open = True
db_channel.total_sent = channel.total_satoshis_sent
db_channel.total_received = channel.total_satoshis_received
pending_out = 0
pending_in = 0
htlc_counter = 0
if len(channel.pending_htlcs) > 0:
for htlc in channel.pending_htlcs:
pending_htlc = PendingHTLCs()
pending_htlc.chan_id = db_channel.chan_id
pending_htlc.alias = db_channel.alias
pending_htlc.incoming = htlc.incoming
pending_htlc.amount = htlc.amount
pending_htlc.hash_lock = htlc.hash_lock.hex()
pending_htlc.expiration_height = htlc.expiration_height
pending_htlc.forwarding_channel = htlc.forwarding_channel
pending_htlc.forwarding_alias = Channels.objects.filter(chan_id=htlc.forwarding_channel)[0].alias if Channels.objects.filter(chan_id=htlc.forwarding_channel).exists() else '---'
pending_htlc.save()
if htlc.incoming == True:
pending_in += htlc.amount
else:
pending_out += htlc.amount
htlc_counter += 1
if htlc.expiration_height - block_height <= 13: # If htlc is expiring within 13 blocks, disconnect peer to help resolve the stuck htlc
peer = Peers.objects.filter(pubkey=channel.remote_pubkey)[0] if Peers.objects.filter(pubkey=channel.remote_pubkey).exists() else None
if peer and (not peer.last_reconnected or (int((datetime.now() - peer.last_reconnected).total_seconds() / 60) > 10)):
logger.info(f'HTLC expiring at {htlc.expiration_height} and within 13 blocks of {block_height}, disconnecting peer {channel.remote_pubkey} to resolve HTLC: {htlc.hash_lock.hex()}')
disconnectpeer(stub, peer)
peer.last_reconnected = datetime.now()
peer.save()
else:
logger.error(f'Could not find peer {channel.remote_pubkey} with expiring HTLC: {htlc.hash_lock.hex()}')
db_channel.pending_outbound = pending_out
db_channel.pending_inbound = pending_in
db_channel.htlc_count = htlc_counter
# Check for peer events
if db_channel.is_active != channel.active:
db_channel.last_update = datetime.now()
peer_alias = Peers.objects.filter(pubkey=db_channel.remote_pubkey)[0].alias if Peers.objects.filter(pubkey=db_channel.remote_pubkey).exists() else None
db_channel.alias = '' if peer_alias is None else peer_alias
if db_channel.is_active is None:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='Connection', old_value=None, new_value=(1 if channel.active else 0), out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
elif channel.active:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='Connection', old_value=0, new_value=1, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
else:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='Connection', old_value=1, new_value=0, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.is_active = channel.active
try:
chan_data = stub.GetChanInfo(ln.ChanInfoRequest(chan_id=channel.chan_id))
if chan_data.node1_pub == channel.remote_pubkey:
local_policy = chan_data.node2_policy
remote_policy = chan_data.node1_policy
else:
local_policy = chan_data.node1_policy
remote_policy = chan_data.node2_policy
old_fee_rate = db_channel.local_fee_rate if db_channel.local_fee_rate is not None else 0
db_channel.local_base_fee = local_policy.fee_base_msat
db_channel.local_fee_rate = local_policy.fee_rate_milli_msat
db_channel.local_cltv = local_policy.time_lock_delta
db_channel.local_disabled = local_policy.disabled
db_channel.local_min_htlc_msat = local_policy.min_htlc
db_channel.local_max_htlc_msat = local_policy.max_htlc_msat
if float(version[:4]) >= 0.18:
try:
db_channel.local_inbound_base_fee = local_policy.inbound_fee_base_msat
db_channel.local_inbound_fee_rate = local_policy.inbound_fee_rate_milli_msat
except:
db_channel.local_inbound_base_fee = 0
db_channel.local_inbound_fee_rate = 0
else:
db_channel.local_inbound_base_fee = 0
db_channel.local_inbound_fee_rate = 0
if db_channel.remote_cltv == -1:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='BaseFee', old_value=None, new_value=remote_policy.fee_base_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_base_fee = remote_policy.fee_base_msat
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='FeeRate', old_value=None, new_value=remote_policy.fee_rate_milli_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_fee_rate = remote_policy.fee_rate_milli_msat
if remote_policy.disabled:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='Disabled', old_value=None, new_value=1, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
else:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='Disabled', old_value=None, new_value=0, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_disabled = remote_policy.disabled
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='CLTV', old_value=None, new_value=remote_policy.time_lock_delta, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_cltv = remote_policy.time_lock_delta
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='MinHTLC', old_value=None, new_value=remote_policy.min_htlc, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_min_htlc_msat = remote_policy.min_htlc
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='MaxHTLC', old_value=None, new_value=remote_policy.max_htlc_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_max_htlc_msat = remote_policy.max_htlc_msat
if float(version[:4]) >= 0.18:
try:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='IncomingBaseFee', old_value=None, new_value=remote_policy.inbound_fee_base_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_inbound_base_fee = remote_policy.inbound_fee_base_msat
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='IncomingFeeRate', old_value=None, new_value=remote_policy.inbound_fee_rate_milli_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_inbound_fee_rate = remote_policy.inbound_fee_rate_milli_msat
except:
db_channel.remote_inbound_base_fee = 0
db_channel.remote_inbound_fee_rate = 0
else:
db_channel.remote_inbound_base_fee = 0
db_channel.remote_inbound_fee_rate = 0
else:
if db_channel.remote_base_fee != remote_policy.fee_base_msat:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='BaseFee', old_value=db_channel.remote_base_fee, new_value=remote_policy.fee_base_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_base_fee = remote_policy.fee_base_msat
if db_channel.remote_fee_rate != remote_policy.fee_rate_milli_msat:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='FeeRate', old_value=db_channel.remote_fee_rate, new_value=remote_policy.fee_rate_milli_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_fee_rate = remote_policy.fee_rate_milli_msat
if db_channel.remote_disabled != remote_policy.disabled:
if db_channel.remote_disabled is None:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='Disabled', old_value=None, new_value=(1 if remote_policy.disabled else 0), out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
elif remote_policy.disabled:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='Disabled', old_value=0, new_value=1, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
else:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='Disabled', old_value=1, new_value=0, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_disabled = remote_policy.disabled
if db_channel.remote_cltv != remote_policy.time_lock_delta:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='CLTV', old_value=db_channel.remote_cltv, new_value=remote_policy.time_lock_delta, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_cltv = remote_policy.time_lock_delta
if db_channel.remote_min_htlc_msat != remote_policy.min_htlc:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='MinHTLC', old_value=db_channel.remote_min_htlc_msat, new_value=remote_policy.min_htlc, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_min_htlc_msat = remote_policy.min_htlc
if db_channel.remote_max_htlc_msat != remote_policy.max_htlc_msat:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='MaxHTLC', old_value=db_channel.remote_max_htlc_msat, new_value=remote_policy.max_htlc_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_max_htlc_msat = remote_policy.max_htlc_msat
if float(version[:4]) >= 0.18:
if db_channel.remote_inbound_base_fee != remote_policy.inbound_fee_base_msat:
try:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='IncomingBaseFee', old_value=db_channel.remote_inbound_base_fee, new_value=remote_policy.inbound_fee_base_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_inbound_base_fee = remote_policy.inbound_fee_base_msat
except:
db_channel.remote_inbound_base_fee = 0
if db_channel.remote_inbound_fee_rate != remote_policy.inbound_fee_rate_milli_msat:
try:
PeerEvents(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, event='IncomingFeeRate', old_value=db_channel.remote_inbound_fee_rate, new_value=remote_policy.inbound_fee_rate_milli_msat, out_liq=(db_channel.local_balance + db_channel.pending_outbound)).save()
db_channel.remote_inbound_fee_rate = remote_policy.inbound_fee_rate_milli_msat
except:
db_channel.remote_inbound_fee_rate = 0
else:
db_channel.remote_inbound_base_fee = 0
db_channel.remote_inbound_fee_rate = 0
except Exception as e: # LND has not found the channel on the graph
logger.error(f'Error getting graph data for channel {db_channel.chan_id}: {str(e)}')
if pending_channel: # skip adding new channel to the list, LND may not have added to the graph yet
logger.error(f'Waiting for pending channel {db_channel.chan_id} to be added to the graph...')
continue
else:
old_fee_rate = None
db_channel.local_base_fee = -1 if db_channel.local_base_fee is None else db_channel.local_base_fee
db_channel.local_fee_rate = -1 if db_channel.local_fee_rate is None else db_channel.local_fee_rate
db_channel.local_cltv = -1 if db_channel.local_cltv is None else db_channel.local_cltv
db_channel.local_disabled = False if db_channel.local_disabled is None else db_channel.local_disabled
db_channel.local_min_htlc_msat = -1 if db_channel.local_min_htlc_msat is None else db_channel.local_min_htlc_msat
db_channel.local_max_htlc_msat = -1 if db_channel.local_max_htlc_msat is None else db_channel.local_max_htlc_msat
db_channel.remote_base_fee = -1 if db_channel.remote_base_fee is None else db_channel.remote_base_fee
db_channel.remote_fee_rate = -1 if db_channel.remote_fee_rate is None else db_channel.remote_fee_rate
db_channel.remote_cltv = -1 if db_channel.remote_cltv is None else db_channel.remote_cltv
db_channel.remote_disabled = False if db_channel.remote_disabled is None else db_channel.remote_disabled
db_channel.remote_min_htlc_msat = -1 if db_channel.remote_min_htlc_msat is None else db_channel.remote_min_htlc_msat
db_channel.remote_max_htlc_msat = -1 if db_channel.remote_max_htlc_msat is None else db_channel.remote_max_htlc_msat
db_channel.local_inbound_base_fee = -1 if db_channel.local_inbound_base_fee is None else db_channel.local_inbound_base_fee
db_channel.local_inbound_fee_rate = -1 if db_channel.local_inbound_fee_rate is None else db_channel.local_inbound_fee_rate
db_channel.remote_inbound_base_fee = -1 if db_channel.remote_inbound_base_fee is None else db_channel.remote_inbound_base_fee
db_channel.remote_inbound_fee_rate = -1 if db_channel.remote_inbound_fee_rate is None else db_channel.remote_inbound_fee_rate
# Check for pending settings to be applied
if pending_channel:
if pending_channel.local_base_fee or pending_channel.local_fee_rate or pending_channel.local_cltv:
base_fee = pending_channel.local_base_fee if pending_channel.local_base_fee else db_channel.local_base_fee
fee_rate = pending_channel.local_fee_rate if pending_channel.local_fee_rate else db_channel.local_fee_rate
cltv = pending_channel.local_cltv if pending_channel.local_cltv else db_channel.local_cltv
channel_point = ln.ChannelPoint()
channel_point.funding_txid_bytes = bytes.fromhex(db_channel.funding_txid)
channel_point.funding_txid_str = db_channel.funding_txid
channel_point.output_index = int(db_channel.output_index)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=base_fee, fee_rate=(fee_rate/1000000), time_lock_delta=cltv))
db_channel.local_base_fee = base_fee
db_channel.local_fee_rate = fee_rate
db_channel.local_cltv = cltv
db_channel.fees_updated = datetime.now()
if pending_channel.auto_rebalance is not None:
db_channel.auto_rebalance = pending_channel.auto_rebalance
if pending_channel.ar_amt_target:
db_channel.ar_amt_target = pending_channel.ar_amt_target
if pending_channel.ar_in_target:
db_channel.ar_in_target = pending_channel.ar_in_target
if pending_channel.ar_out_target:
db_channel.ar_out_target = pending_channel.ar_out_target
if pending_channel.ar_max_cost:
db_channel.ar_max_cost = pending_channel.ar_max_cost
if pending_channel.auto_fees is not None:
db_channel.auto_fees = pending_channel.auto_fees
pending_channel.delete()
if old_fee_rate is not None and old_fee_rate != local_policy.fee_rate_milli_msat:
logger.info(f'Ext fee change detected on {db_channel.chan_id} for peer {db_channel.alias}: fee updated from {old_fee_rate} to {db_channel.local_fee_rate}')
# External Fee change detected, update auto fee log
db_channel.fees_updated = datetime.now()
Autofees(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, setting=('Ext'), old_value=old_fee_rate, new_value=db_channel.local_fee_rate).save()
db_channel.save()
counter += 1
chan_list.append(channel.chan_id)
records = Channels.objects.filter(is_open=True).count()
if records > counter:
# A channel must have been closed, mark it as closed
channels = Channels.objects.filter(is_open=True).exclude(chan_id__in=chan_list)
for channel in channels:
channel.last_update = datetime.now()
channel.is_active = False
channel.is_open = False
channel.save()
def update_peers(stub):
counter = 0
peer_list = []
peers = stub.ListPeers(ln.ListPeersRequest(latest_error=True)).peers
for peer in peers:
exists = 1 if Peers.objects.filter(pubkey=peer.pub_key).count() == 1 else 0
if exists == 1:
db_peer = Peers.objects.filter(pubkey=peer.pub_key)[0]
db_peer.pubkey = peer.pub_key
db_peer.address = peer.address
db_peer.sat_sent = peer.sat_sent
db_peer.sat_recv = peer.sat_recv
db_peer.inbound = peer.inbound
db_peer.ping_time = round(peer.ping_time/1000)
if db_peer.connected == False:
try:
db_peer.alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer.pub_key, include_channels=False)).node.alias
except:
db_peer.alias = ''
db_peer.connected = True
db_peer.save()
elif exists == 0:
try:
alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer.pub_key, include_channels=False)).node.alias
except:
alias = ''
Peers(pubkey = peer.pub_key, address = peer.address, sat_sent = peer.sat_sent, sat_recv = peer.sat_recv, inbound = peer.inbound, ping_time = round(peer.ping_time/1000), alias=alias, connected = True).save()
counter += 1
peer_list.append(peer.pub_key)
records = Peers.objects.filter(connected=True).count()
if records > counter:
disconnected = Peers.objects.filter(connected=True).exclude(pubkey__in=peer_list)
for peer in disconnected:
peer.connected = False
peer.save()
def update_onchain(stub):
Onchain.objects.filter(block_height=0).delete()
last_block = 0 if Onchain.objects.aggregate(Max('block_height'))['block_height__max'] == None else Onchain.objects.aggregate(Max('block_height'))['block_height__max'] + 1
onchain_txs = stub.GetTransactions(ln.GetTransactionsRequest(start_height=last_block)).transactions
for tx in onchain_txs:
Onchain(tx_hash=tx.tx_hash, time_stamp=datetime.fromtimestamp(tx.time_stamp), amount=tx.amount, fee=tx.total_fees, block_hash=tx.block_hash, block_height=tx.block_height, label=tx.label[:100]).save()
def network_links():
if LocalSettings.objects.filter(key='GUI-NetLinks').exists():
network_links = str(LocalSettings.objects.filter(key='GUI-NetLinks')[0].value)
else:
LocalSettings(key='GUI-NetLinks', value='https://mempool.space').save()
network_links = 'https://mempool.space'
return network_links
def get_tx_fees(txid):
base_url = network_links() + ('/testnet' if settings.LND_NETWORK == 'testnet' else '') + '/api/tx/'
try:
request_data = get(base_url + txid).json()
fee = request_data['fee']
except Exception as e:
logger.error(f'Error getting closure fees for {txid}: {str(e)}')
fee = 0
return fee
def update_closures(stub):
closures = stub.ClosedChannels(ln.ClosedChannelsRequest()).channels
if len(closures) > Closures.objects.all().count():
counter = 0
skip = Closures.objects.all().count()
for closure in closures:
counter += 1
if counter > skip:
resolution_count = len(closure.resolutions)
txid, index = closure.channel_point.split(':')
closing_costs = get_tx_fees(closure.closing_tx_hash) if (closure.open_initiator != 2 and closure.close_type not in [4, 5]) else 0
db_closure = Closures(chan_id=closure.chan_id, funding_txid=txid, funding_index=index, closing_tx=closure.closing_tx_hash, remote_pubkey=closure.remote_pubkey, capacity=closure.capacity, close_height=closure.close_height, settled_balance=closure.settled_balance, time_locked_balance=closure.time_locked_balance, close_type=closure.close_type, open_initiator=closure.open_initiator, close_initiator=closure.close_initiator, resolution_count=resolution_count)
try:
db_closure.save()
except Exception as e:
logger.error(f'Error inserting closure: {str(e)}')
Closures.objects.filter(funding_txid=txid,funding_index=index).delete()
return
if resolution_count > 0:
Resolutions.objects.filter(chan_id=closure.chan_id).delete()
for resolution in closure.resolutions:
if resolution.resolution_type != 2 and not Resolutions.objects.filter(sweep_txid=resolution.sweep_txid).exists():
closing_costs += get_tx_fees(resolution.sweep_txid)
Resolutions(chan_id=closure.chan_id, resolution_type=resolution.resolution_type, outcome=resolution.outcome, outpoint_tx=resolution.outpoint.txid_str, outpoint_index=resolution.outpoint.output_index, amount_sat=resolution.amount_sat, sweep_txid=resolution.sweep_txid).save()
db_closure.closing_costs = closing_costs
db_closure.save()
def reconnect_peers(stub):
if LocalSettings.objects.filter(key='LND-ReconnectInterval').exists():
reconnect_interval = int(LocalSettings.objects.filter(key='LND-ReconnectInterval')[0].value)
else:
LocalSettings(key='LND-ReconnectInterval', value='3').save()
reconnect_interval = 3
inactive_peers = Channels.objects.filter(is_open=True, is_active=False, private=False).values_list('remote_pubkey', flat=True).distinct()
if len(inactive_peers) > 0:
peers = Peers.objects.all()
for inactive_peer in inactive_peers:
if peers.filter(pubkey=inactive_peer).exists():
peer = peers.filter(pubkey=inactive_peer)[0]
if peer.last_reconnected is None or ((datetime.now() - peer.last_reconnected).total_seconds() >= (reconnect_interval + random.uniform(0, reconnect_interval)) * 60):
logger.info(f'Reconnecting peer {peer.alias} {peer.pubkey}, last reconnected at {peer.last_reconnected}')
if peer.connected == True:
logger.info(f'Inactive channel is still connected to peer, disconnecting peer {peer.alias} {inactive_peer}')
disconnectpeer(stub, peer)
try:
node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=inactive_peer, include_channels=False)).node
host = node.addresses[0].addr
except Exception as e:
logger.error(f'Unable to find node info on graph, using last known value for {peer.alias} {peer.pubkey} at {peer.address}: {str(e)}')
host = peer.address
logger.info(f'Attempting connection to {peer.alias} {inactive_peer} at {host}')
try:
# try both the graph value and last know value
stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=ln.LightningAddress(pubkey=inactive_peer, host=host), perm=True, timeout=5))
if host != peer.address and peer.address[:9] != '127.0.0.1':
stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=ln.LightningAddress(pubkey=inactive_peer, host=peer.address), perm=True, timeout=5))
except Exception as e:
error = str(e)
details_index = error.find('details =') + 11
debug_error_index = error.find('debug_error_string =') - 3
error_msg = error[details_index:debug_error_index]
logger.error(f'Error reconnecting {peer.alias} {inactive_peer}: {error_msg}')
peer.last_reconnected = datetime.now()
peer.save()
def clean_payments(stub):
if LocalSettings.objects.filter(key='LND-CleanPayments').exists():
enabled = int(LocalSettings.objects.filter(key='LND-CleanPayments')[0].value)
else:
LocalSettings(key='LND-CleanPayments', value='0').save()
enabled = 0
if enabled == 1:
if LocalSettings.objects.filter(key='LND-RetentionDays').exists():
retention_days = int(LocalSettings.objects.filter(key='LND-RetentionDays')[0].value)
else:
LocalSettings(key='LND-RetentionDays', value='30').save()
retention_days = 30
time_filter = datetime.now() - timedelta(days=retention_days)
target_payments = Payments.objects.exclude(status=1).filter(cleaned=False).filter(creation_date__lte=time_filter).order_by('index')[:10]
for payment in target_payments:
payment_hash = bytes.fromhex(payment.payment_hash)
htlcs_only = True if payment.status == 2 else False
try:
stub.DeletePayment(ln.DeletePaymentRequest(payment_hash=payment_hash, failed_htlcs_only=htlcs_only))
except Exception as e:
error = str(e)
details_index = error.find('details =') + 11
debug_error_index = error.find('debug_error_string =') - 3
error_msg = error[details_index:debug_error_index]
logger.error(f'Error cleaning payment {payment.payment_hash} at index {payment.index} with payment status {payment.status}: {error_msg}')
finally:
payment.cleaned = True
payment.save()
def auto_fees(stub):
if LocalSettings.objects.filter(key='AF-Enabled').exists():
if int(LocalSettings.objects.filter(key='AF-Enabled')[0].value) == 0: #disabled
return
else:
LocalSettings(key='AF-Enabled', value='0').save()
return
if LocalSettings.objects.filter(key='AF-InboundFees').exists():
inbound_enabled = int(LocalSettings.objects.filter(key='AF-InboundFees')[0].value)
else:
LocalSettings(key='AF-InboundFees', value='0').save()
inbound_enabled = False
try:
channels = Channels.objects.filter(is_open=True, is_active=True, private=False, auto_fees=True)
results_df = af.main(channels)
if not results_df.empty:
update_df = results_df[results_df['eligible'] == True]
update_df = update_df[(update_df['adjustment']!=0) | (update_df['inbound_adjustment']!=0)]
if not update_df.empty:
for target_channel in update_df.to_dict(orient='records'):
channel = Channels.objects.filter(chan_id=target_channel['chan_id'])[0]
channel_point = ln.ChannelPoint()
channel_point.funding_txid_bytes = bytes.fromhex(channel.funding_txid)
channel_point.funding_txid_str = channel.funding_txid
channel_point.output_index = channel.output_index
version = stub.GetInfo(ln.GetInfoRequest()).version
if inbound_enabled and float(version[:4]) >= 0.18:
inbound_fee_rate = int(target_channel['new_inbound_rate'])
# if we are using a discount, then discount our base fee to mirror outbound
if inbound_fee_rate == 0:
inbound_base_fee = 0
else:
inbound_base_fee = -channel.local_base_fee
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=channel.local_base_fee, fee_rate=(target_channel['new_rate']/1000000), time_lock_delta=channel.local_cltv, inbound_fee=ln.InboundFee(base_fee_msat=inbound_base_fee, fee_rate_ppm=inbound_fee_rate)))
if target_channel['inbound_adjustment'] != 0:
logger.info(f'Updating inbound fees for channel {str(target_channel["chan_id"])} to a value of: {str(target_channel["new_inbound_rate"])}')
channel.local_inbound_fee_rate = target_channel['new_inbound_rate']
InboundFeeLog(chan_id=channel.chan_id, peer_alias=channel.alias, setting=(f"AF [ {target_channel['net_routed_7day']}:{target_channel['in_percent']}:{target_channel['out_percent']} ]"), old_value=target_channel['local_inbound_fee_rate'], new_value=target_channel['new_inbound_rate']).save()
else:
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=channel.local_base_fee, fee_rate=(target_channel['new_rate']/1000000), time_lock_delta=channel.local_cltv))
if target_channel['adjustment'] != 0:
logger.info(f'Updating outbound fees for channel {str(target_channel["chan_id"])} to a value of: {str(target_channel["new_rate"])}')
channel.local_fee_rate = target_channel['new_rate']
Autofees(chan_id=channel.chan_id, peer_alias=channel.alias, setting=(f"AF [ {target_channel['net_routed_7day']}:{target_channel['in_percent']}:{target_channel['out_percent']} ]"), old_value=target_channel['local_fee_rate'], new_value=target_channel['new_rate']).save()
channel.fees_updated = datetime.now()
channel.save()
except Exception as e:
logger.error(f'Error processing auto_fees: {str(e)}')
def agg_htlcs(target_htlcs, category):
try:
target_ids = target_htlcs.values_list('id')
agg_htlcs = FailedHTLCs.objects.filter(id__in=target_ids).annotate(day=TruncDay('timestamp')).values('day', 'chan_id_in', 'chan_id_out').annotate(amount=Sum('amount'), fee=Sum('missed_fee'), liq=Avg('chan_out_liq'), pending=Avg('chan_out_pending'), count=Count('id'), chan_in_alias=Max('chan_in_alias'), chan_out_alias=Max('chan_out_alias'))
for htlc in agg_htlcs:
if HistFailedHTLC.objects.filter(date=htlc['day'],chan_id_in=htlc['chan_id_in'],chan_id_out=htlc['chan_id_out']).exists():
htlc_itm = HistFailedHTLC.objects.filter(date=htlc['day'],chan_id_in=htlc['chan_id_in'],chan_id_out=htlc['chan_id_out']).get()
else:
htlc_itm = HistFailedHTLC(htlc_count=0, amount_sum=0, fee_sum=0, liq_avg=0, pending_avg=0, balance_count=0, downstream_count=0, other_count=0)
htlc_itm.date = htlc['day']
htlc_itm.chan_id_in = htlc['chan_id_in']
htlc_itm.chan_id_out = htlc['chan_id_out']
htlc_itm.chan_in_alias = htlc['chan_in_alias']
htlc_itm.chan_out_alias = htlc['chan_out_alias']
htlc_itm.htlc_count += htlc['count']
htlc_itm.amount_sum += htlc['amount']
htlc_itm.fee_sum += htlc['fee']
htlc_itm.liq_avg += (htlc['count']/htlc_itm.htlc_count)*((0 if htlc['liq'] is None else htlc['liq'])-htlc_itm.liq_avg)
htlc_itm.pending_avg += (htlc['count']/htlc_itm.htlc_count)*((0 if htlc['pending'] is None else htlc['pending'])-htlc_itm.pending_avg)
if category == 'balance':
htlc_itm.balance_count += htlc['count']
elif category == 'downstream':
htlc_itm.downstream_count += htlc['count']
elif category == 'other':
htlc_itm.other_count += htlc['count']
htlc_itm.save()
FailedHTLCs.objects.filter(id__in=target_ids, chan_id_in=htlc['chan_id_in'], chan_id_out=htlc['chan_id_out']).annotate(day=TruncDay('timestamp')).filter(day=htlc['day']).delete()
except Exception as e:
logger.error(f'Error processing agg_htlcs: {str(e)}')
def agg_failed_htlcs():
time_filter = datetime.now() - timedelta(days=30)
agg_htlcs(FailedHTLCs.objects.filter(timestamp__lte=time_filter, failure_detail=6)[:100], 'balance')
agg_htlcs(FailedHTLCs.objects.filter(timestamp__lte=time_filter, failure_detail=99)[:100], 'downstream')
agg_htlcs(FailedHTLCs.objects.filter(timestamp__lte=time_filter).exclude(failure_detail__in=[6, 99])[:100], 'other')
def main():
while True:
logger.info('Starting data execution...')
try:
stub = lnrpc.LightningStub(lnd_connect())
# Update data
update_peers(stub)
update_channels(stub)
update_invoices(stub)
update_payments(stub)
update_forwards(stub)
update_onchain(stub)
update_closures(stub)
reconnect_peers(stub)
clean_payments(stub)
auto_fees(stub)
agg_failed_htlcs()
except Exception as e:
logger.error(f'Error processing background data: {str(e)}')
try:
from django.db import connections
connections.close_all()
except Exception as db_err:
logger.error(f"Error closing database connections: {str(db_err)}")
finally:
logger.info('Data execution completed...sleeping for 20 seconds')
sleep(20)
if __name__ == '__main__':
main()