lndg/gui/views.py

3588 lines
No EOL
233 KiB
Python

from django.contrib import messages
from django.shortcuts import get_object_or_404, render, redirect
from django.db.models import Sum, IntegerField, Count, Max, F, Q, Case, When, Value, FloatField, ExpressionWrapper, DateTimeField, DurationField
from django.db.models.functions import Round, TruncDay, Coalesce
from django.utils import timezone
from django.contrib.auth.decorators import login_required
from django_filters import FilterSet, CharFilter, DateTimeFilter, NumberFilter
from datetime import datetime, timedelta
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from .forms import *
from .serializers import *
from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, Closures, Resolutions, PendingHTLCs, FailedHTLCs, Autopilot, Autofees, InboundFeeLog, PendingChannels, AvoidNodes, PeerEvents, HistFailedHTLC, TradeSales
from gui.lnd_deps import lightning_pb2 as ln
from gui.lnd_deps import lightning_pb2_grpc as lnrpc
from gui.lnd_deps import router_pb2 as lnr
from gui.lnd_deps import router_pb2_grpc as lnrouter
from gui.lnd_deps import wtclient_pb2 as wtrpc
from gui.lnd_deps import wtclient_pb2_grpc as wtstub
from gui.lnd_deps import walletkit_pb2 as walletrpc
from gui.lnd_deps import walletkit_pb2_grpc as walletstub
from gui.lnd_deps.lnd_connect import lnd_connect
from lndg import settings
from os import path
from pandas import DataFrame, merge
from requests import get
from secrets import token_bytes
from trade import create_trade_details
import af
import logging
logger = logging.getLogger('django.lndg')
def graph_links():
if LocalSettings.objects.filter(key='GUI-GraphLinks').exists():
graph_links = str(LocalSettings.objects.filter(key='GUI-GraphLinks')[0].value)
else:
LocalSettings(key='GUI-GraphLinks', value='https://mempool.space/lightning').save()
graph_links = 'https://mempool.space/lightning'
return graph_links
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/'
request_data = get(base_url + txid).json()
fee = request_data['fee']
return fee
def pending_channel_details(channel_point):
funding_txid, output_index = channel_point.split(':')
if Channels.objects.filter(funding_txid=funding_txid, output_index=output_index).exists():
channel = Channels.objects.filter(funding_txid=funding_txid, output_index=output_index)[0]
return {'short_chan_id':channel.short_chan_id,'chan_id':channel.chan_id,'alias':channel.alias}
else:
return {'short_chan_id':None,'chan_id':None,'alias':None}
class is_login_required(object):
def __init__(self, dec, condition):
self.decorator = dec
self.condition = condition
def __call__(self, func):
if not self.condition:
# No login required
return func
return self.decorator(func)
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def home(request):
if request.method != 'GET':
return redirect('home')
try:
stub = lnrpc.LightningStub(lnd_connect())
node_info = stub.GetInfo(ln.GetInfoRequest())
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
return render(request, 'home.html', {
'node_info': {'color': node_info.color, 'alias': node_info.alias, 'version': node_info.version, 'identity_pubkey': node_info.identity_pubkey, 'uris': node_info.uris},
'local_settings': get_local_settings('AR-'),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links(),
'network_links': network_links(),
})
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def channels(request):
if request.method == 'GET':
filter_7day = datetime.now() - timedelta(days=7)
filter_30day = datetime.now() - timedelta(days=30)
forwards = Forwards.objects.filter(forward_date__gte=filter_30day)
payments = Payments.objects.filter(status=2, creation_date__gte=filter_30day, rebal_chan__isnull=False)
invoices = Invoices.objects.filter(state=1, r_hash__in=payments.values_list('payment_hash'))
channels = Channels.objects.filter(is_open=True, private=False)
channels_df = DataFrame.from_records(channels.values())
if channels_df.shape[0] > 0:
forwards_df_30d = DataFrame.from_records(forwards.values())
forwards_df_7d = DataFrame.from_records(forwards.filter(forward_date__gte=filter_7day).values())
forwards_df_in_30d_sum = DataFrame() if forwards_df_30d.empty else forwards_df_30d.groupby('chan_id_in', as_index=True).sum(numeric_only=True)
forwards_df_out_30d_sum = DataFrame() if forwards_df_30d.empty else forwards_df_30d.groupby('chan_id_out', as_index=True).sum(numeric_only=True)
forwards_df_in_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_in', as_index=True).sum(numeric_only=True)
forwards_df_out_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_out', as_index=True).sum(numeric_only=True)
forwards_df_in_30d_count = DataFrame() if forwards_df_30d.empty else forwards_df_30d.groupby('chan_id_in', as_index=True).count()
forwards_df_out_30d_count = DataFrame() if forwards_df_30d.empty else forwards_df_30d.groupby('chan_id_out', as_index=True).count()
forwards_df_in_7d_count = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_in', as_index=True).count()
forwards_df_out_7d_count = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_out', as_index=True).count()
payments_df_30d = DataFrame.from_records(payments.values())
payments_df_7d = DataFrame.from_records(payments.filter(creation_date__gte=filter_7day).values())
payments_df_30d_sum = DataFrame() if payments_df_30d.empty else payments_df_30d.groupby('chan_out', as_index=True).sum(numeric_only=True)
payments_df_7d_sum = DataFrame() if payments_df_7d.empty else payments_df_7d.groupby('chan_out', as_index=True).sum(numeric_only=True)
payments_df_30d_count = DataFrame() if payments_df_30d.empty else payments_df_30d.groupby('chan_out', as_index=True).count()
payments_df_7d_count = DataFrame() if payments_df_7d.empty else payments_df_7d.groupby('chan_out', as_index=True).count()
invoices_df_30d = DataFrame.from_records(invoices.values())
invoices_df_7d = DataFrame.from_records(invoices.filter(settle_date__gte=filter_7day).values())
invoices_df_30d_sum = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True).sum(numeric_only=True)
invoices_df_7d_sum = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True).sum(numeric_only=True)
invoices_df_30d_count = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True).count()
invoices_df_7d_count = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True).count()
invoice_hashes_7d = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True)['r_hash'].apply(list)
invoice_hashes_30d = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True)['r_hash'].apply(list)
channels_df['mil_capacity'] = channels_df.apply(lambda row: round(row.capacity/1000000, 1), axis=1)
channels_df['local_balance'] = channels_df.apply(lambda row: row.local_balance + row.pending_outbound, axis=1)
channels_df['remote_balance'] = channels_df.apply(lambda row: row.remote_balance + row.pending_inbound, axis=1)
channels_df['routed_in_7day'] = channels_df.apply(lambda row: forwards_df_in_7d_count.loc[row.chan_id].amt_out_msat if (forwards_df_in_7d_count.index == row.chan_id).any() else 0, axis=1)
channels_df['routed_out_7day'] = channels_df.apply(lambda row: forwards_df_out_7d_count.loc[row.chan_id].amt_out_msat if (forwards_df_out_7d_count.index == row.chan_id).any() else 0, axis=1)
channels_df['routed_in_30day'] = channels_df.apply(lambda row: forwards_df_in_30d_count.loc[row.chan_id].amt_out_msat if (forwards_df_in_30d_count.index == row.chan_id).any() else 0, axis=1)
channels_df['routed_out_30day'] = channels_df.apply(lambda row: forwards_df_out_30d_count.loc[row.chan_id].amt_out_msat if (forwards_df_out_30d_count.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_routed_in_7day'] = channels_df.apply(lambda row: int(forwards_df_in_7d_sum.loc[row.chan_id].amt_out_msat/100000000)/10 if (forwards_df_in_7d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_routed_out_7day'] = channels_df.apply(lambda row: int(forwards_df_out_7d_sum.loc[row.chan_id].amt_out_msat/100000000)/10 if (forwards_df_out_7d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_routed_in_30day'] = channels_df.apply(lambda row: int(forwards_df_in_30d_sum.loc[row.chan_id].amt_out_msat/100000000)/10 if (forwards_df_in_30d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_routed_out_30day'] = channels_df.apply(lambda row: int(forwards_df_out_30d_sum.loc[row.chan_id].amt_out_msat/100000000)/10 if (forwards_df_out_30d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['rebal_in_30day'] = channels_df.apply(lambda row: invoices_df_30d_count.loc[row.chan_id].amt_paid if invoices_df_30d_count.empty == False and (invoices_df_30d_count.index == row.chan_id).any() else 0, axis=1)
channels_df['rebal_out_30day'] = channels_df.apply(lambda row: payments_df_30d_count.loc[row.chan_id].value if payments_df_30d_count.empty == False and (payments_df_30d_count.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_rebal_in_30day'] = channels_df.apply(lambda row: int(invoices_df_30d_sum.loc[row.chan_id].amt_paid/100000)/10 if invoices_df_30d_count.empty == False and (invoices_df_30d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_rebal_out_30day'] = channels_df.apply(lambda row: int(payments_df_30d_sum.loc[row.chan_id].value/100000)/10 if payments_df_30d_count.empty == False and (payments_df_30d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['rebal_in_7day'] = channels_df.apply(lambda row: invoices_df_7d_count.loc[row.chan_id].amt_paid if invoices_df_7d_count.empty == False and (invoices_df_7d_count.index == row.chan_id).any() else 0, axis=1)
channels_df['rebal_out_7day'] = channels_df.apply(lambda row: payments_df_7d_count.loc[row.chan_id].value if payments_df_7d_count.empty == False and (payments_df_7d_count.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_rebal_in_7day'] = channels_df.apply(lambda row: int(invoices_df_7d_sum.loc[row.chan_id].amt_paid/100000)/10 if invoices_df_7d_count.empty == False and (invoices_df_7d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_rebal_out_7day'] = channels_df.apply(lambda row: int(payments_df_7d_sum.loc[row.chan_id].value/100000)/10 if payments_df_7d_count.empty == False and (payments_df_7d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['revenue_7day'] = channels_df.apply(lambda row: int(forwards_df_out_7d_sum.loc[row.chan_id].fee) if forwards_df_out_7d_sum.empty == False and (forwards_df_out_7d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['revenue_30day'] = channels_df.apply(lambda row: int(forwards_df_out_30d_sum.loc[row.chan_id].fee) if forwards_df_out_30d_sum.empty == False and (forwards_df_out_30d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['revenue_assist_7day'] = channels_df.apply(lambda row: int(forwards_df_in_7d_sum.loc[row.chan_id].fee) if forwards_df_in_7d_sum.empty == False and (forwards_df_in_7d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['revenue_assist_30day'] = channels_df.apply(lambda row: int(forwards_df_in_30d_sum.loc[row.chan_id].fee) if forwards_df_in_30d_sum.empty == False and (forwards_df_in_30d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['costs_7day'] = channels_df.apply(lambda row: 0 if row['rebal_in_7day'] == 0 else int(payments_df_7d.set_index('payment_hash', inplace=False).loc[invoice_hashes_7d[row.chan_id] if invoice_hashes_7d.empty == False and (invoice_hashes_7d.index == row.chan_id).any() else []]['fee'].sum()), axis=1)
channels_df['costs_30day'] = channels_df.apply(lambda row: 0 if row['rebal_in_30day'] == 0 else int(payments_df_30d.set_index('payment_hash', inplace=False).loc[invoice_hashes_30d[row.chan_id] if invoice_hashes_30d.empty == False and (invoice_hashes_30d.index == row.chan_id).any() else []]['fee'].sum()), axis=1)
channels_df['profits_7day'] = channels_df.apply(lambda row: row['revenue_7day'] - row['costs_7day'], axis=1)
channels_df['profits_30day'] = channels_df.apply(lambda row: row['revenue_30day'] - row['costs_30day'], axis=1)
channels_df['open_block'] = channels_df.apply(lambda row: int(row.chan_id)>>40, axis=1)
apy_7day = round((channels_df['profits_7day'].sum()*5214.2857)/channels_df['local_balance'].sum(), 2)
apy_30day = round((channels_df['profits_30day'].sum()*1216.6667)/channels_df['local_balance'].sum(), 2)
active_updates = channels_df['num_updates'].sum()
channels_df['updates'] = channels_df.apply(lambda row: 0 if active_updates == 0 else int(round((row['num_updates']/active_updates)*100, 0)), axis=1)
node_outbound = channels_df['local_balance'].sum()
node_capacity = channels_df['capacity'].sum()
if node_capacity > 0:
outbound_ratio = node_outbound/node_capacity
channels_df['apy_7day'] = channels_df.apply(lambda row: round((row['profits_7day']*5214.2857)/(row['capacity']*outbound_ratio), 2), axis=1)
channels_df['apy_30day'] = channels_df.apply(lambda row: round((row['profits_30day']*1216.6667)/(row['capacity']*outbound_ratio), 2), axis=1)
channels_df['cv_7day'] = channels_df.apply(lambda row: round((row['revenue_7day']*5214.2857)/(row['capacity']*outbound_ratio) + (row['revenue_assist_7day']*5214.2857)/(row['capacity']*(1-outbound_ratio)), 2), axis=1)
channels_df['cv_30day'] = channels_df.apply(lambda row: round((row['revenue_30day']*1216.6667)/(row['capacity']*outbound_ratio) + (row['revenue_assist_30day']*1216.6667)/(row['capacity']*(1-outbound_ratio)), 2), axis=1)
else:
channels_df['apy_7day'] = 0.0
channels_df['apy_30day'] = 0.0
channels_df['cv_7day'] = 0.0
channels_df['cv_30day'] = 0.0
else:
apy_7day = 0
apy_30day = 0
context = {
'channels': [] if channels_df.empty else channels_df.sort_values(by=['cv_30day'], ascending=False).to_dict(orient='records'),
'apy_7day': apy_7day,
'apy_30day': apy_30day,
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links(),
'network_links': network_links()
}
return render(request, 'channels.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def fees(request):
if request.method == 'GET':
channels = Channels.objects.filter(is_open=True, private=False)
results_df = af.main(channels)
context = {
'channels': [] if results_df.empty else results_df.sort_values(by=['out_percent']).to_dict(orient='records'),
'local_settings': get_local_settings('AF-'),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links(),
'network_links': network_links()
}
return render(request, 'fee_rates.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def advanced(request):
if request.method == 'GET':
channels = Channels.objects.filter(is_open=True).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*1000)/Sum('capacity'), inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*1000)/Sum('capacity')).order_by('-is_active', 'outbound_percent')
channels_df = DataFrame.from_records(channels.values())
if channels_df.shape[0] > 0:
channels_df['out_percent'] = channels_df.apply(lambda row: int(round(row['outbound_percent']/10, 0)), axis=1)
channels_df['in_percent'] = channels_df.apply(lambda row: int(round(row['inbound_percent']/10, 0)), axis=1)
channels_df['local_balance'] = channels_df.apply(lambda row: row.local_balance + row.pending_outbound, axis=1)
channels_df['remote_balance'] = channels_df.apply(lambda row: row.remote_balance + row.pending_inbound, axis=1)
channels_df['fee_ratio'] = channels_df.apply(lambda row: 100 if row['local_fee_rate'] == 0 else int(round(((row['remote_fee_rate']/row['local_fee_rate'])*1000)/10, 0)), axis=1)
channels_df['local_min_htlc'] = channels_df['local_min_htlc_msat']/1000
channels_df['local_max_htlc'] = channels_df['local_max_htlc_msat']/1000
context = {
'channels': channels_df.to_dict(orient='records'),
'local_settings': get_local_settings('AF-', 'AR-', 'GUI-', 'LND-'),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links(),
'network_links': network_links()
}
return render(request, 'advanced.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def logs(request):
if request.method == 'GET':
try:
count = request.GET.get('tail', 20)
grep = request.GET.get('grep', None)
logfile = path.join(settings.BASE_DIR, 'data/lndg-controller.log')
file_size = path.getsize(logfile)-2
if file_size == 0:
logs = ['Logs are empty....']
else:
target_size = 128*int(count)
read_size = min(target_size, file_size)
with open(logfile, "rb") as reader:
reader.seek(-read_size, 2)
logs = []
for line in reader.readlines():
log_line = line.decode('utf-8')
if grep:
if str(grep) in log_line:
logs.append(log_line)
else:
logs.append(log_line)
return render(request, 'logs.html', {'logs': logs})
except Exception as e:
return render(request, 'error.html', {'error': str(e)})
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def route(request):
if request.method == 'GET':
try:
stub = lnrpc.LightningStub(lnd_connect())
block_height = stub.GetInfo(ln.GetInfoRequest()).block_height
payment_hash = request.GET.urlencode()[1:]
route = PaymentHops.objects.filter(payment_hash=payment_hash).annotate(ppm=Round((Sum('fee')/Sum('amt'))*1000000, output_field=IntegerField())) if PaymentHops.objects.filter(payment_hash=payment_hash).exists() else None
total_cost = round(route.aggregate(Sum('fee'))['fee__sum'],3) if route is not None else 0
total_ppm = int(total_cost*1000000/route.filter(step=1).aggregate(Sum('amt'))['amt__sum']) if route is not None else 0
context = {
'payment_hash': payment_hash,
'total_cost': total_cost,
'total_ppm': total_ppm,
'route': route,
'invoices': Invoices.objects.filter(r_hash=payment_hash),
'incoming_htlcs': PendingHTLCs.objects.filter(incoming=True, hash_lock=payment_hash).annotate(blocks_til_expiration=Sum('expiration_height')-block_height, hours_til_expiration=((Sum('expiration_height')-block_height)*10)/60).order_by('hash_lock'),
'outgoing_htlcs': PendingHTLCs.objects.filter(incoming=False, hash_lock=payment_hash).annotate(blocks_til_expiration=Sum('expiration_height')-block_height, hours_til_expiration=((Sum('expiration_height')-block_height)*10)/60).order_by('hash_lock')
}
return render(request, 'route.html', context)
except Exception as e:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def routes(request):
if request.method == 'GET':
try:
pubkey = request.GET.urlencode()[1:]
context = {
'payment_hash': pubkey,
'route': PaymentHops.objects.filter(payment_hash__in=PaymentHops.objects.filter(node_pubkey=pubkey).order_by('-id').values_list('payment_hash')[:69]).annotate(ppm=Round((Sum('fee')/Sum('amt'))*1000000, output_field=IntegerField()))
}
return render(request, 'route.html', context)
except Exception as e:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def peers(request):
if request.method == 'GET':
peers = Peers.objects.filter(connected=True)
context = {
'peers': peers,
'num_peers': len(peers),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links()
}
return render(request, 'peers.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def balances(request):
if request.method == 'GET':
stub = walletstub.WalletKitStub(lnd_connect())
pending_sweeps = stub.PendingSweeps(walletrpc.PendingSweepsRequest()).pending_sweeps
sweeps = []
for pending_sweep in pending_sweeps:
sweep = {}
sweep['txid_str'] = pending_sweep.outpoint.txid_bytes[::-1].hex()
sweep['txid_index'] = pending_sweep.outpoint.output_index
sweep['amount_sat'] = pending_sweep.amount_sat
sweep['witness_type'] = pending_sweep.witness_type
sweep['requested_sat_per_vbyte'] = pending_sweep.requested_sat_per_vbyte
sweep['sat_per_vbyte'] = pending_sweep.sat_per_vbyte
sweep['requested_conf_target'] = pending_sweep.requested_conf_target
sweep['broadcast_attempts'] = pending_sweep.broadcast_attempts
sweep['next_broadcast_height'] = pending_sweep.next_broadcast_height
sweep['force'] = pending_sweep.force
sweeps.append(sweep)
context = {
'utxos': stub.ListUnspent(walletrpc.ListUnspentRequest(min_confs=0, max_confs=9999999)).utxos,
'transactions': list(Onchain.objects.filter(block_height=0)) + list(Onchain.objects.exclude(block_height=0).order_by('-block_height')),
'pending_sweeps': sweeps,
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'network_links': network_links()
}
return render(request, 'balances.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def closures(request):
if request.method == 'GET':
try:
stub = lnrpc.LightningStub(lnd_connect())
pending_channels = stub.PendingChannels(ln.PendingChannelsRequest())
pending_closed = None
pending_force_closed = None
waiting_for_close = None
if pending_channels.pending_closing_channels:
target_resp = pending_channels.pending_closing_channels
pending_closed = []
for i in range(0,len(target_resp)):
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,
'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type, 'local_commit_fee_sat': target_resp[i].commitments.local_commit_fee_sat, 'limbo_balance':target_resp[i].limbo_balance, 'closing_txid':target_resp[i].closing_txid}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
pending_closed.append(pending_item)
if pending_channels.pending_force_closing_channels:
target_resp = pending_channels.pending_force_closing_channels
pending_force_closed = []
for i in range(0,len(target_resp)):
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'initiator':target_resp[i].channel.initiator,
'commitment_type':target_resp[i].channel.commitment_type,'closing_txid':target_resp[i].closing_txid,'limbo_balance':target_resp[i].limbo_balance,'maturity_height':target_resp[i].maturity_height,'blocks_til_maturity':target_resp[i].blocks_til_maturity if target_resp[i].blocks_til_maturity > 0 else find_next_block_maturity(target_resp[i]),
'maturity_datetime':(datetime.now()+timedelta(minutes=(10*target_resp[i].blocks_til_maturity if target_resp[i].blocks_til_maturity > 0 else 10*find_next_block_maturity(target_resp[i]) )))}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
pending_force_closed.append(pending_item)
if pending_channels.waiting_close_channels:
target_resp = pending_channels.waiting_close_channels
waiting_for_close = []
for i in range(0,len(target_resp)):
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,
'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type, 'local_commit_fee_sat': target_resp[i].commitments.local_commit_fee_sat, 'limbo_balance':target_resp[i].limbo_balance, 'closing_txid':target_resp[i].closing_txid}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
waiting_for_close.append(pending_item)
closures_df = DataFrame.from_records(Closures.objects.all().values())
if closures_df.empty:
merged = DataFrame()
else:
channels_df = DataFrame.from_records(Channels.objects.all().values('chan_id', 'short_chan_id', 'alias'))
if channels_df.empty:
merged = closures_df
merged['alias'] = ''
merged['short_chan_id'] = merged.apply(lambda row: str(int(row.chan_id) >> 40) + 'x' + str(int(int(row.chan_id) >> 16) & 0xFFFFFF) + 'x' + str(int(row.chan_id) & 0xFFFF), axis=1)
else:
merged = merge(closures_df, channels_df, on='chan_id', how='left')
merged['alias'] = merged['alias'].fillna('')
merged['short_chan_id'] = merged['short_chan_id'].fillna('')
merged['short_chan_id'] = merged.apply(lambda row: str(int(row.chan_id) >> 40) + 'x' + str(int(int(row.chan_id) >> 16) & 0xFFFFFF) + 'x' + str(int(row.chan_id) & 0xFFFF) if row.short_chan_id == '' else row.short_chan_id, axis=1)
context = {
'pending_closed': pending_closed,
'pending_force_closed': pending_force_closed,
'waiting_for_close': waiting_for_close,
'closures': [] if merged.empty else merged.sort_values(by=['close_height'], ascending=False).to_dict(orient='records'),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'network_links': network_links(),
'graph_links': graph_links()
}
return render(request, 'closures.html', context)
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
def find_next_block_maturity(force_closing_channel):
if force_closing_channel.blocks_til_maturity > 0:
return force_closing_channel.blocks_til_maturity
for pending_htlc in force_closing_channel.pending_htlcs:
if pending_htlc.blocks_til_maturity > 0:
return pending_htlc.blocks_til_maturity
return -1
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def towers(request):
if request.method == 'GET':
try:
stub = wtstub.WatchtowerClientStub(lnd_connect())
towers = stub.ListTowers(wtrpc.ListTowersRequest(include_sessions=False)).towers
active_towers = []
inactive_towers = []
for item in towers:
for address in item.addresses:
tower = {}
tower['pubkey'] = item.pubkey.hex()
tower['address'] = address
tower['active'] = item.active_session_candidate
tower['num_sessions'] = item.num_sessions
active_towers.append(tower) if tower['active'] else inactive_towers.append(tower)
stats = stub.Stats(wtrpc.StatsRequest())
context = {
'active_towers': active_towers,
'inactive_towers': inactive_towers,
'stats': stats
}
return render(request, 'towers.html', context)
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def add_tower_form(request):
if request.method == 'POST':
form = AddTowerForm(request.POST)
if form.is_valid():
try:
stub = wtstub.WatchtowerClientStub(lnd_connect())
tower = form.cleaned_data['tower']
if tower.count('@') == 1 and len(tower.split('@')[0]) == 66:
peer_pubkey, host = tower.split('@')
else:
raise Exception('Invalid tower connection string.')
response = stub.AddTower(wtrpc.AddTowerRequest(pubkey=bytes.fromhex(peer_pubkey), address=host))
messages.success(request, 'Tower addition successful!' + str(response))
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]
messages.error(request, 'Add tower request failed! Error: ' + error_msg)
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def delete_tower_form(request):
if request.method == 'POST':
form = DeleteTowerForm(request.POST)
if form.is_valid():
try:
stub = wtstub.WatchtowerClientStub(lnd_connect())
pubkey = bytes.fromhex(form.cleaned_data['pubkey'])
address = form.cleaned_data['address']
response = stub.RemoveTower(wtrpc.RemoveTowerRequest(pubkey=pubkey, address=address))
messages.success(request, 'Tower deletion successful!' + str(response))
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]
messages.error(request, 'Delete tower request failed! Error: ' + error_msg)
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def remove_tower_form(request):
if request.method == 'POST':
form = RemoveTowerForm(request.POST)
if form.is_valid():
try:
stub = wtstub.WatchtowerClientStub(lnd_connect())
pubkey = bytes.fromhex(form.cleaned_data['pubkey'])
response = stub.RemoveTower(wtrpc.RemoveTowerRequest(pubkey=pubkey))
messages.success(request, 'Tower removal successful!' + str(response))
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]
messages.error(request, 'Remove tower request failed! Error: ' + error_msg)
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def resolutions(request):
if request.method == 'GET':
chan_id = request.GET.urlencode()[1:]
context = {
'chan_id': chan_id,
'resolutions': Resolutions.objects.filter(chan_id=chan_id),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'network_links': network_links()
}
return render(request, 'resolutions.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def income(request):
if request.method == 'GET':
stub = lnrpc.LightningStub(lnd_connect())
filter_90day = datetime.now() - timedelta(days=90)
filter_30day = datetime.now() - timedelta(days=30)
filter_7day = datetime.now() - timedelta(days=7)
filter_1day = datetime.now() - timedelta(days=1)
node_info = stub.GetInfo(ln.GetInfoRequest())
invoices = Invoices.objects.filter(state=1, is_revenue=True)
invoices_90day = invoices.filter(settle_date__gte=filter_90day)
invoices_30day = invoices.filter(settle_date__gte=filter_30day)
invoices_7day = invoices.filter(settle_date__gte=filter_7day)
invoices_1day = invoices.filter(settle_date__gte=filter_1day)
payments = Payments.objects.filter(status=2)
payments_90day = payments.filter(creation_date__gte=filter_90day)
payments_30day = payments.filter(creation_date__gte=filter_30day)
payments_7day = payments.filter(creation_date__gte=filter_7day)
payments_1day = payments.filter(creation_date__gte=filter_1day)
onchain_txs = Onchain.objects.all()
onchain_txs_90day = onchain_txs.filter(time_stamp__gte=filter_90day)
onchain_txs_30day = onchain_txs.filter(time_stamp__gte=filter_30day)
onchain_txs_7day = onchain_txs.filter(time_stamp__gte=filter_7day)
onchain_txs_1day = onchain_txs.filter(time_stamp__gte=filter_1day)
closures = Closures.objects.all()
closures_90day = Closures.objects.filter(close_height__gte=(node_info.block_height - 12960))
closures_30day = Closures.objects.filter(close_height__gte=(node_info.block_height - 4320))
closures_7day = Closures.objects.filter(close_height__gte=(node_info.block_height - 1008))
closures_1day = Closures.objects.filter(close_height__gte=(node_info.block_height - 144))
forwards = Forwards.objects.all()
forwards_90day = forwards.filter(forward_date__gte=filter_90day)
forwards_30day = forwards.filter(forward_date__gte=filter_30day)
forwards_7day = forwards.filter(forward_date__gte=filter_7day)
forwards_1day = forwards.filter(forward_date__gte=filter_1day)
forward_count = forwards.count()
forward_count_90day = forwards_90day.count()
forward_count_30day = forwards_30day.count()
forward_count_7day = forwards_7day.count()
forward_count_1day = forwards_1day.count()
forward_amount = 0 if forward_count == 0 else int(forwards.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000)
forward_amount_90day = 0 if forward_count_90day == 0 else int(forwards_90day.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000)
forward_amount_30day = 0 if forward_count_30day == 0 else int(forwards_30day.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000)
forward_amount_7day = 0 if forward_count_7day == 0 else int(forwards_7day.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000)
forward_amount_1day = 0 if forward_count_1day == 0 else int(forwards_1day.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000)
total_revenue = 0 if forward_count == 0 else int(forwards.aggregate(Sum('fee'))['fee__sum'])
total_revenue_90day = 0 if forward_count_90day == 0 else int(forwards_90day.aggregate(Sum('fee'))['fee__sum'])
total_revenue_30day = 0 if forward_count_30day == 0 else int(forwards_30day.aggregate(Sum('fee'))['fee__sum'])
total_revenue_7day = 0 if forward_count_7day == 0 else int(forwards_7day.aggregate(Sum('fee'))['fee__sum'])
total_revenue_1day = 0 if forward_count_1day == 0 else int(forwards_1day.aggregate(Sum('fee'))['fee__sum'])
total_received = 0 if invoices.count() == 0 else int(invoices.aggregate(Sum('amt_paid'))['amt_paid__sum'])
total_received_90day = 0 if invoices_90day.count() == 0 else int(invoices_90day.aggregate(Sum('amt_paid'))['amt_paid__sum'])
total_received_30day = 0 if invoices_30day.count() == 0 else int(invoices_30day.aggregate(Sum('amt_paid'))['amt_paid__sum'])
total_received_7day = 0 if invoices_7day.count() == 0 else int(invoices_7day.aggregate(Sum('amt_paid'))['amt_paid__sum'])
total_received_1day = 0 if invoices_1day.count() == 0 else int(invoices_1day.aggregate(Sum('amt_paid'))['amt_paid__sum'])
total_revenue += total_received
total_revenue_90day += total_received_90day
total_revenue_30day += total_received_30day
total_revenue_7day += total_received_7day
total_revenue_1day += total_received_1day
total_revenue_ppm = 0 if forward_amount == 0 else int(total_revenue/(forward_amount/1000000))
total_revenue_ppm_90day = 0 if forward_amount_90day == 0 else int(total_revenue_90day/(forward_amount_90day/1000000))
total_revenue_ppm_30day = 0 if forward_amount_30day == 0 else int(total_revenue_30day/(forward_amount_30day/1000000))
total_revenue_ppm_7day = 0 if forward_amount_7day == 0 else int(total_revenue_7day/(forward_amount_7day/1000000))
total_revenue_ppm_1day = 0 if forward_amount_1day == 0 else int(total_revenue_1day/(forward_amount_1day/1000000))
total_sent = 0 if payments.count() == 0 else int(payments.aggregate(Sum('value'))['value__sum'])
total_sent_90day = 0 if payments_90day.count() == 0 else int(payments_90day.aggregate(Sum('value'))['value__sum'])
total_sent_30day = 0 if payments_30day.count() == 0 else int(payments_30day.aggregate(Sum('value'))['value__sum'])
total_sent_7day = 0 if payments_7day.count() == 0 else int(payments_7day.aggregate(Sum('value'))['value__sum'])
total_sent_1day = 0 if payments_1day.count() == 0 else int(payments_1day.aggregate(Sum('value'))['value__sum'])
total_fees = 0 if payments.count() == 0 else int(payments.aggregate(Sum('fee'))['fee__sum'])
total_fees_90day = 0 if payments_90day.count() == 0 else int(payments_90day.aggregate(Sum('fee'))['fee__sum'])
total_fees_30day = 0 if payments_30day.count() == 0 else int(payments_30day.aggregate(Sum('fee'))['fee__sum'])
total_fees_7day = 0 if payments_7day.count() == 0 else int(payments_7day.aggregate(Sum('fee'))['fee__sum'])
total_fees_1day = 0 if payments_1day.count() == 0 else int(payments_1day.aggregate(Sum('fee'))['fee__sum'])
total_fees_ppm = 0 if total_sent == 0 else int(total_fees/(total_sent/1000000))
total_fees_ppm_90day = 0 if total_sent_90day == 0 else int(total_fees_90day/(total_sent_90day/1000000))
total_fees_ppm_30day = 0 if total_sent_30day == 0 else int(total_fees_30day/(total_sent_30day/1000000))
total_fees_ppm_7day = 0 if total_sent_7day == 0 else int(total_fees_7day/(total_sent_7day/1000000))
total_fees_ppm_1day = 0 if total_sent_1day == 0 else int(total_fees_1day/(total_sent_1day/1000000))
onchain_costs = 0 if onchain_txs.count() == 0 else onchain_txs.aggregate(Sum('fee'))['fee__sum']
onchain_costs_90day = 0 if onchain_txs_90day.count() == 0 else onchain_txs_90day.aggregate(Sum('fee'))['fee__sum']
onchain_costs_30day = 0 if onchain_txs_30day.count() == 0 else onchain_txs_30day.aggregate(Sum('fee'))['fee__sum']
onchain_costs_7day = 0 if onchain_txs_7day.count() == 0 else onchain_txs_7day.aggregate(Sum('fee'))['fee__sum']
onchain_costs_1day = 0 if onchain_txs_1day.count() == 0 else onchain_txs_1day.aggregate(Sum('fee'))['fee__sum']
close_fees = closures.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures.exists() else 0
close_fees_90day = closures_90day.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_90day.exists() else 0
close_fees_30day = closures_30day.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_30day.exists() else 0
close_fees_7day = closures_7day.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_7day.exists() else 0
close_fees_1day = closures_1day.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_1day.exists() else 0
onchain_costs += close_fees
onchain_costs_90day += close_fees_90day
onchain_costs_30day += close_fees_30day
onchain_costs_7day += close_fees_7day
onchain_costs_1day += close_fees_1day
profits = int(total_revenue-total_fees-onchain_costs)
profits_90day = int(total_revenue_90day-total_fees_90day-onchain_costs_90day)
profits_30day = int(total_revenue_30day-total_fees_30day-onchain_costs_30day)
profits_7day = int(total_revenue_7day-total_fees_7day-onchain_costs_7day)
profits_1day = int(total_revenue_1day-total_fees_1day-onchain_costs_1day)
context = {
'node_info': node_info,
'forward_count': forward_count,
'forward_count_90day': forward_count_90day,
'forward_count_30day': forward_count_30day,
'forward_count_7day': forward_count_7day,
'forward_count_1day': forward_count_1day,
'forward_amount': forward_amount,
'forward_amount_90day': forward_amount_90day,
'forward_amount_30day': forward_amount_30day,
'forward_amount_7day': forward_amount_7day,
'forward_amount_1day': forward_amount_1day,
'total_revenue': total_revenue,
'total_revenue_90day': total_revenue_90day,
'total_revenue_30day': total_revenue_30day,
'total_revenue_7day': total_revenue_7day,
'total_revenue_1day': total_revenue_1day,
'total_fees': total_fees,
'total_fees_90day': total_fees_90day,
'total_fees_30day': total_fees_30day,
'total_fees_7day': total_fees_7day,
'total_fees_1day': total_fees_1day,
'total_fees_ppm': total_fees_ppm,
'total_fees_ppm_90day': total_fees_ppm_90day,
'total_fees_ppm_30day': total_fees_ppm_30day,
'total_fees_ppm_7day': total_fees_ppm_7day,
'total_fees_ppm_1day': total_fees_ppm_1day,
'onchain_costs': onchain_costs,
'onchain_costs_90day': onchain_costs_90day,
'onchain_costs_30day': onchain_costs_30day,
'onchain_costs_7day': onchain_costs_7day,
'onchain_costs_1day': onchain_costs_1day,
'total_revenue_ppm': total_revenue_ppm,
'total_revenue_ppm_90day': total_revenue_ppm_90day,
'total_revenue_ppm_30day': total_revenue_ppm_30day,
'total_revenue_ppm_7day': total_revenue_ppm_7day,
'total_revenue_ppm_1day': total_revenue_ppm_1day,
'profits': profits,
'profits_90day': profits_90day,
'profits_30day': profits_30day,
'profits_7day': profits_7day,
'profits_1day': profits_1day,
'profits_ppm': 0 if forward_amount == 0 else int(profits/(forward_amount/1000000)),
'profits_ppm_90day': 0 if forward_amount_90day == 0 else int(profits_90day/(forward_amount_90day/1000000)),
'profits_ppm_30day': 0 if forward_amount_30day == 0 else int(profits_30day/(forward_amount_30day/1000000)),
'profits_ppm_7day': 0 if forward_amount_7day == 0 else int(profits_7day/(forward_amount_7day/1000000)),
'profits_ppm_1day': 0 if forward_amount_1day == 0 else int(profits_1day/(forward_amount_1day/1000000)),
'percent_cost': 0 if total_revenue == 0 else int(((total_fees+onchain_costs)/total_revenue)*100),
'percent_cost_90day': 0 if total_revenue_90day == 0 else int(((total_fees_90day+onchain_costs_90day)/total_revenue_90day)*100),
'percent_cost_30day': 0 if total_revenue_30day == 0 else int(((total_fees_30day+onchain_costs_30day)/total_revenue_30day)*100),
'percent_cost_7day': 0 if total_revenue_7day == 0 else int(((total_fees_7day+onchain_costs_7day)/total_revenue_7day)*100),
'percent_cost_1day': 0 if total_revenue_1day == 0 else int(((total_fees_1day+onchain_costs_1day)/total_revenue_1day)*100),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links()
}
return render(request, 'income.html', context)
else:
return redirect('home')
@api_view(['GET'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def chart(request):
payments = Payments.objects.filter(status=2).annotate(dt=TruncDay('creation_date')).values('dt').annotate(cost=Sum('fee', output_field=FloatField()), revenue=Value(0, output_field=FloatField()), onchain=Value(0))
invoices = Invoices.objects.filter(state=1, is_revenue=True).annotate(dt=TruncDay('settle_date')).values('dt').annotate(cost=Value(0, output_field=FloatField()), revenue=Sum('amt_paid', output_field=FloatField()), onchain=Value(0))
forwards = Forwards.objects.annotate(dt=TruncDay('forward_date')).values('dt').annotate(cost=Value(0, output_field=FloatField()), revenue=Sum('fee', output_field=FloatField()), onchain=Value(0))
onchain = Onchain.objects.annotate(dt=TruncDay('time_stamp')).values('dt').annotate(cost=Value(0, output_field=FloatField()), revenue=Value(0, output_field=FloatField()), onchain=Sum('fee'))
# Estimate blockchain timing parameters
first_record = Onchain.objects.order_by('time_stamp').first()
first_date = first_record.time_stamp
first_block = first_record.block_height
last_record = Onchain.objects.order_by('time_stamp').last()
last_date = last_record.time_stamp
last_block = last_record.block_height
time_interval_per_block = timedelta(minutes=10)
if last_block > first_block:
time_interval_per_block = (last_date - first_date) / (last_block - first_block)
offset = first_date - first_block * time_interval_per_block
# Convert close_height to datetime
datetime_from_blocks = TruncDay(
ExpressionWrapper(
offset
+ ExpressionWrapper(
F('close_height') * time_interval_per_block,
output_field=DurationField(),
),
output_field=DateTimeField()
)
)
closures = Closures.objects.annotate(dt=datetime_from_blocks).values('dt').annotate(cost=Value(0, output_field=FloatField()), revenue=Value(0, output_field=FloatField()), onchain=Sum('closing_costs'))
balance = DataFrame.from_records(payments.union(invoices, forwards, onchain, closures).values('dt', 'cost', 'revenue', 'onchain'))
results = balance.groupby('dt').sum().reset_index().sort_values('dt')
return Response(results.to_dict(orient='records'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def channel(request):
if request.method == 'GET':
chan_id = request.GET.urlencode()[1:]
if Channels.objects.filter(chan_id=chan_id).exists():
filter_1day = datetime.now() - timedelta(days=1)
filter_7day = datetime.now() - timedelta(days=7)
filter_30day = datetime.now() - timedelta(days=30)
forwards_df = DataFrame.from_records(Forwards.objects.filter(Q(chan_id_in=chan_id) | Q(chan_id_out=chan_id)).values())
payments_df = DataFrame.from_records(Payments.objects.filter(status=2).filter(chan_out=chan_id).filter(rebal_chan__isnull=False).annotate(ppm=Round((Sum('fee')*1000000)/Sum('value'), output_field=IntegerField())).values())
rebal_payments = Payments.objects.filter(status=2).filter(rebal_chan=chan_id)
invoices_df = DataFrame.from_records(Invoices.objects.filter(state=1).filter(chan_in=chan_id).filter(r_hash__in=rebal_payments).values())
channels_df = DataFrame.from_records(Channels.objects.filter(is_open=True).values())
if channels_df.empty:
node_outbound = 0
node_capacity = 0
else:
node_outbound = channels_df['local_balance'].sum()
node_capacity = channels_df['capacity'].sum()
channel = Channels.objects.filter(chan_id=chan_id)
channels_df = DataFrame.from_records(channel.values())
rebalancer_df = DataFrame.from_records(Rebalancer.objects.filter(last_hop_pubkey=channels_df['remote_pubkey'][0]).annotate(ppm=Round((Sum('fee_limit')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-id').values())
failed_htlc_df = DataFrame.from_records(FailedHTLCs.objects.exclude(wire_failure=99).filter(Q(chan_id_in=chan_id) | Q(chan_id_out=chan_id)).order_by('-id').values())
peer_info_df = DataFrame.from_records(Peers.objects.filter(pubkey=channels_df['remote_pubkey'][0]).values())
channels_df['local_balance'] = channels_df['local_balance'] + channels_df['pending_outbound']
channels_df['remote_balance'] = channels_df['remote_balance'] + channels_df['pending_inbound']
channels_df['in_percent'] = ((channels_df['remote_balance']/channels_df['capacity'])*100).round(0).astype(int)
channels_df['out_percent'] = ((channels_df['local_balance']/channels_df['capacity'])*100).round(0).astype(int)
channels_df['open_block'] = channels_df['chan_id'].apply(lambda row: int(row)>>40).astype(int)
channels_df['routed_in'] = 0
channels_df['routed_in_30day'] = 0
channels_df['routed_in_7day'] = 0
channels_df['routed_in_1day'] = 0
channels_df['routed_out'] = 0
channels_df['routed_out_30day'] = 0
channels_df['routed_out_7day'] = 0
channels_df['routed_out_1day'] = 0
channels_df['amt_routed_in'] = 0
channels_df['amt_routed_in_30day'] = 0
channels_df['amt_routed_in_7day'] = 0
channels_df['amt_routed_in_7day_fees'] = 0
channels_df['amt_routed_in_1day'] = 0
channels_df['amt_routed_out'] = 0
channels_df['amt_routed_out_30day'] = 0
channels_df['amt_routed_out_7day'] = 0
channels_df['amt_routed_out_7day_fees'] = 0
channels_df['amt_routed_out_1day'] = 0
channels_df['average_in'] = 0
channels_df['average_in_30day'] = 0
channels_df['average_in_7day'] = 0
channels_df['average_in_1day'] = 0
channels_df['average_out'] = 0
channels_df['average_out_30day'] = 0
channels_df['average_out_7day'] = 0
channels_df['average_out_1day'] = 0
channels_df['revenue'] = 0
channels_df['revenue_30day'] = 0
channels_df['revenue_7day'] = 0
channels_df['revenue_7day_fees'] = 0
channels_df['revenue_1day'] = 0
channels_df['revenue_assist'] = 0
channels_df['revenue_assist_30day'] = 0
channels_df['revenue_assist_7day'] = 0
channels_df['revenue_assist_7day_fees'] = 0
channels_df['revenue_assist_1day'] = 0
channels_df['rebal_out'] = 0
channels_df['rebal_out_30day'] = 0
channels_df['rebal_out_7day'] = 0
channels_df['rebal_out_1day'] = 0
channels_df['amt_rebal_out'] = 0
channels_df['amt_rebal_out_30day'] = 0
channels_df['amt_rebal_out_7day'] = 0
channels_df['amt_rebal_out_1day'] = 0
channels_df['rebal_in'] = 0
channels_df['rebal_in_30day'] = 0
channels_df['rebal_in_7day'] = 0
channels_df['rebal_in_1day'] = 0
channels_df['amt_rebal_in'] = 0
channels_df['amt_rebal_in_30day'] = 0
channels_df['amt_rebal_in_7day'] = 0
channels_df['amt_rebal_in_1day'] = 0
channels_df['costs'] = 0
channels_df['costs_30day'] = 0
channels_df['costs_7day'] = 0
channels_df['costs_1day'] = 0
channels_df['attempts'] = 0
channels_df['attempts_30day'] = 0
channels_df['attempts_7day'] = 0
channels_df['attempts_1day'] = 0
channels_df['success'] = 0
channels_df['success_30day'] = 0
channels_df['success_7day'] = 0
channels_df['success_1day'] = 0
channels_df['success_rate'] = 0
channels_df['success_rate_30day'] = 0
channels_df['success_rate_7day'] = 0
channels_df['success_rate_1day'] = 0
channels_df['failed_out'] = 0
channels_df['failed_out_30day'] = 0
channels_df['failed_out_7day'] = 0
channels_df['failed_out_1day'] = 0
start_date = None
if failed_htlc_df.shape[0]> 0:
channels_df['failed_out'] = len(failed_htlc_df[(failed_htlc_df['chan_id_out']==chan_id) & (failed_htlc_df['wire_failure']==15) & (failed_htlc_df['failure_detail']==6) & (failed_htlc_df['amount']>failed_htlc_df['chan_out_liq']+failed_htlc_df['chan_out_pending'])])
failed_htlc_df_30d = failed_htlc_df.loc[failed_htlc_df['timestamp'] >= filter_30day]
if failed_htlc_df_30d.shape[0]> 0:
channels_df['failed_out_30day'] = len(failed_htlc_df_30d[(failed_htlc_df_30d['chan_id_out']==chan_id) & (failed_htlc_df_30d['wire_failure']==15) & (failed_htlc_df_30d['failure_detail']==6) & (failed_htlc_df_30d['amount']>failed_htlc_df_30d['chan_out_liq']+failed_htlc_df_30d['chan_out_pending'])])
failed_htlc_df_7d = failed_htlc_df_30d.loc[failed_htlc_df_30d['timestamp'] >= filter_7day]
if failed_htlc_df_7d.shape[0]> 0:
channels_df['failed_out_7day'] = len(failed_htlc_df_7d[(failed_htlc_df_7d['chan_id_out']==chan_id) & (failed_htlc_df_7d['wire_failure']==15) & (failed_htlc_df_7d['failure_detail']==6) & (failed_htlc_df_7d['amount']>failed_htlc_df_7d['chan_out_liq']+failed_htlc_df_7d['chan_out_pending'])])
failed_htlc_df_1d = failed_htlc_df_7d.loc[failed_htlc_df_7d['timestamp'] >= filter_1day]
if failed_htlc_df_1d.shape[0]> 0:
channels_df['failed_out_1day'] = len(failed_htlc_df_1d[(failed_htlc_df_1d['chan_id_out']==chan_id) & (failed_htlc_df_1d['wire_failure']==15) & (failed_htlc_df_1d['failure_detail']==6) & (failed_htlc_df_1d['amount']>failed_htlc_df_1d['chan_out_liq']+failed_htlc_df_1d['chan_out_pending'])])
if rebalancer_df.shape[0]> 0:
channels_df['attempts'] = len(rebalancer_df[(rebalancer_df['status']>=2) & (rebalancer_df['status']<400)])
channels_df['success'] = len(rebalancer_df[rebalancer_df['status']==2])
channels_df['success_rate'] = 0 if channels_df['attempts'].iloc[0] == 0 else ((channels_df['success']/channels_df['attempts'])*100).astype(int)
rebalancer_df_30d = rebalancer_df.loc[rebalancer_df['stop'] >= filter_30day]
if rebalancer_df_30d.shape[0]> 0:
channels_df['attempts_30day'] = len(rebalancer_df_30d[(rebalancer_df_30d['status']>=2) & (rebalancer_df_30d['status']<400)])
channels_df['success_30day'] = len(rebalancer_df_30d[rebalancer_df_30d['status']==2])
channels_df['success_rate_30day'] = 0 if channels_df['attempts_30day'].iloc[0] == 0 else ((channels_df['success_30day']/channels_df['attempts_30day'])*100).astype(int)
rebalancer_df_7d = rebalancer_df_30d.loc[rebalancer_df_30d['stop'] >= filter_7day]
if rebalancer_df_7d.shape[0]> 0:
channels_df['attempts_7day'] = len(rebalancer_df_7d[(rebalancer_df_7d['status']>=2) & (rebalancer_df_7d['status']<400)])
channels_df['success_7day'] = len(rebalancer_df_7d[rebalancer_df_7d['status']==2])
channels_df['success_rate_7day'] = 0 if channels_df['attempts_7day'].iloc[0] == 0 else ((channels_df['success_7day']/channels_df['attempts_7day'])*100).astype(int)
rebalancer_df_1d = rebalancer_df_7d.loc[rebalancer_df_7d['stop'] >= filter_1day]
if rebalancer_df_1d.shape[0]> 0:
channels_df['attempts_1day'] = len(rebalancer_df_1d[(rebalancer_df_1d['status']>=2) & (rebalancer_df_1d['status']<400)])
channels_df['success_1day'] = len(rebalancer_df_1d[rebalancer_df_1d['status']==2])
channels_df['success_rate_1day'] = 0 if channels_df['attempts_1day'].iloc[0] == 0 else ((channels_df['success_1day']/channels_df['attempts_1day'])*100).astype(int)
if forwards_df.shape[0]> 0:
forwards_in_df = forwards_df[forwards_df['chan_id_in'] == chan_id]
forwards_out_df = forwards_df[forwards_df['chan_id_out'] == chan_id]
forwards_df['amt_in'] = (forwards_df['amt_in_msat']/1000).astype(int)
forwards_df['amt_out'] = (forwards_df['amt_out_msat']/1000).astype(int)
forwards_df['ppm'] = (forwards_df['fee']/(forwards_df['amt_out']/1000000)).astype(int)
else:
forwards_in_df = DataFrame()
forwards_out_df = DataFrame()
if forwards_in_df.shape[0]> 0:
forwards_in_df_count = forwards_in_df.groupby('chan_id_in', as_index=True).count()
forwards_in_df_sum = forwards_in_df.groupby('chan_id_in', as_index=True).sum(numeric_only=True)
channels_df['routed_in'] = forwards_in_df_count.loc[chan_id].amt_out_msat
channels_df['amt_routed_in'] = int(forwards_in_df_sum.loc[chan_id].amt_out_msat/1000)
channels_df['average_in'] = 0 if channels_df['routed_in'].iloc[0] == 0 else (channels_df['amt_routed_in']/channels_df['routed_in']).astype(int)
channels_df['revenue_assist'] = int(forwards_in_df_sum.loc[chan_id].fee) if forwards_in_df_sum.empty == False else 0
forwards_in_df_30d = forwards_in_df.loc[forwards_in_df['forward_date'] >= filter_30day]
if forwards_in_df_30d.shape[0] > 0:
forwards_in_df_30d_count = forwards_in_df_30d.groupby('chan_id_in', as_index=True).count()
forwards_in_df_30d_sum = forwards_in_df_30d.groupby('chan_id_in', as_index=True).sum(numeric_only=True)
channels_df['routed_in_30day'] = forwards_in_df_30d_count.loc[chan_id].amt_out_msat
channels_df['amt_routed_in_30day'] = int(forwards_in_df_30d_sum.loc[chan_id].amt_out_msat/1000)
channels_df['average_in_30day'] = 0 if channels_df['routed_in_30day'].iloc[0] == 0 else (channels_df['amt_routed_in_30day']/channels_df['routed_in_30day']).astype(int)
channels_df['revenue_assist_30day'] = int(forwards_in_df_30d_sum.loc[chan_id].fee) if forwards_in_df_30d_sum.empty == False else 0
forwards_in_df_7d = forwards_in_df_30d.loc[forwards_in_df_30d['forward_date'] >= filter_7day]
if forwards_in_df_7d.shape[0] > 0:
forwards_in_df_7d_count = forwards_in_df_7d.groupby('chan_id_in', as_index=True).count()
forwards_in_df_7d_sum = forwards_in_df_7d.groupby('chan_id_in', as_index=True).sum(numeric_only=True)
channels_df['routed_in_7day'] = forwards_in_df_7d_count.loc[chan_id].amt_out_msat
channels_df['amt_routed_in_7day'] = int(forwards_in_df_7d_sum.loc[chan_id].amt_out_msat/1000)
channels_df['average_in_7day'] = 0 if channels_df['routed_in_7day'].iloc[0] == 0 else (channels_df['amt_routed_in_7day']/channels_df['routed_in_7day']).astype(int)
channels_df['revenue_assist_7day'] = int(forwards_in_df_7d_sum.loc[chan_id].fee)
forwards_in_df_7d_sum = forwards_in_df_7d[forwards_in_df_7d['amt_out_msat']>=1000000].groupby('chan_id_in', as_index=True).sum(numeric_only=True)
if forwards_in_df_7d_sum.shape[0] > 0:
channels_df['amt_routed_in_7day_fees'] = int(forwards_in_df_7d_sum.loc[chan_id].amt_out_msat/1000)
channels_df['revenue_assist_7day_fees'] = int(forwards_in_df_7d_sum.loc[chan_id].fee)
forwards_in_df_1d = forwards_in_df_7d.loc[forwards_in_df_7d['forward_date'] >= filter_1day]
if forwards_in_df_1d.shape[0] > 0:
forwards_in_df_1d_count = forwards_in_df_1d.groupby('chan_id_in', as_index=True).count()
forwards_in_df_1d_sum = forwards_in_df_1d.groupby('chan_id_in', as_index=True).sum(numeric_only=True)
channels_df['routed_in_1day'] = forwards_in_df_1d_count.loc[chan_id].amt_out_msat
channels_df['amt_routed_in_1day'] = int(forwards_in_df_1d_sum.loc[chan_id].amt_out_msat/1000)
channels_df['average_in_1day'] = 0 if channels_df['routed_in_1day'].iloc[0] == 0 else (channels_df['amt_routed_in_1day']/channels_df['routed_in_1day']).astype(int)
channels_df['revenue_assist_1day'] = int(forwards_in_df_1d_sum.loc[chan_id].fee)
if forwards_out_df.shape[0]> 0:
start_date = forwards_out_df['forward_date'].min()
forwards_out_df_count = forwards_out_df.groupby('chan_id_out', as_index=True).count()
forwards_out_df_sum = forwards_out_df.groupby('chan_id_out', as_index=True).sum(numeric_only=True)
channels_df['routed_out'] = forwards_out_df_count.loc[chan_id].amt_out_msat
channels_df['amt_routed_out'] = int(forwards_out_df_sum.loc[chan_id].amt_out_msat/1000)
channels_df['average_out'] = 0 if channels_df['routed_out'].iloc[0] == 0 else (channels_df['amt_routed_out']/channels_df['routed_out']).astype(int)
channels_df['revenue'] = int(forwards_out_df_sum.loc[chan_id].fee) if forwards_out_df_sum.empty == False else 0
forwards_out_df_30d = forwards_out_df.loc[forwards_out_df['forward_date'] >= filter_30day]
if forwards_out_df_30d.shape[0] > 0:
forwards_out_df_30d_count = forwards_out_df_30d.groupby('chan_id_out', as_index=True).count()
forwards_out_df_30d_sum = forwards_out_df_30d.groupby('chan_id_out', as_index=True).sum(numeric_only=True)
channels_df['routed_out_30day'] = forwards_out_df_30d_count.loc[chan_id].amt_out_msat
channels_df['amt_routed_out_30day'] = int(forwards_out_df_30d_sum.loc[chan_id].amt_out_msat/1000)
channels_df['average_out_30day'] = 0 if channels_df['routed_out_30day'].iloc[0] == 0 else (channels_df['amt_routed_out_30day']/channels_df['routed_out_30day']).astype(int)
channels_df['revenue_30day'] = int(forwards_out_df_30d_sum.loc[chan_id].fee) if forwards_out_df_30d_sum.empty == False else 0
forwards_out_df_7d = forwards_out_df_30d.loc[forwards_out_df_30d['forward_date'] >= filter_7day]
if forwards_out_df_7d.shape[0] > 0:
forwards_out_df_7d_count = forwards_out_df_7d.groupby('chan_id_out', as_index=True).count()
forwards_out_df_7d_sum = forwards_out_df_7d.groupby('chan_id_out', as_index=True).sum(numeric_only=True)
channels_df['routed_out_7day'] = forwards_out_df_7d_count.loc[chan_id].amt_out_msat
channels_df['amt_routed_out_7day'] = int(forwards_out_df_7d_sum.loc[chan_id].amt_out_msat/1000)
channels_df['average_out_7day'] = 0 if channels_df['routed_out_7day'].iloc[0] == 0 else (channels_df['amt_routed_out_7day']/channels_df['routed_out_7day']).astype(int)
channels_df['revenue_7day'] = int(forwards_out_df_7d_sum.loc[chan_id].fee)
forwards_out_df_7d_sum = forwards_out_df_7d[forwards_out_df_7d['amt_out_msat']>=1000000].groupby('chan_id_out', as_index=True).sum(numeric_only=True)
if forwards_out_df_7d_sum.shape[0] > 0:
channels_df['amt_routed_out_7day_fees'] = int(forwards_out_df_7d_sum.loc[chan_id].amt_out_msat/1000)
channels_df['revenue_7day_fees'] = int(forwards_out_df_7d_sum.loc[chan_id].fee)
forwards_out_df_1d = forwards_out_df_7d.loc[forwards_out_df_7d['forward_date'] >= filter_1day]
if forwards_out_df_1d.shape[0] > 0:
forwards_out_df_1d_count = forwards_out_df_1d.groupby('chan_id_out', as_index=True).count()
forwards_out_df_1d_sum = forwards_out_df_1d.groupby('chan_id_out', as_index=True).sum(numeric_only=True)
channels_df['routed_out_1day'] = forwards_out_df_1d_count.loc[chan_id].amt_out_msat
channels_df['amt_routed_out_1day'] = int(forwards_out_df_1d_sum.loc[chan_id].amt_out_msat/1000)
channels_df['average_out_1day'] = 0 if channels_df['routed_out_1day'].iloc[0] == 0 else (channels_df['amt_routed_out_1day']/channels_df['routed_out_1day']).astype(int)
channels_df['revenue_1day'] = int(forwards_out_df_1d_sum.loc[chan_id].fee)
if payments_df.shape[0] > 0:
payments_df_count = payments_df.groupby('chan_out', as_index=True).count()
payments_df_sum = payments_df.groupby('chan_out', as_index=True).sum(numeric_only=True)
channels_df['rebal_out'] = payments_df_count.loc[chan_id].value
channels_df['amt_rebal_out'] = int(payments_df_sum.loc[chan_id].value)
payments_df_30d = payments_df.loc[payments_df['creation_date'] >= filter_30day]
if payments_df_30d.shape[0] > 0:
payments_df_30d_count = payments_df_30d.groupby('chan_out', as_index=True).count()
payments_df_30d_sum = payments_df_30d.groupby('chan_out', as_index=True).sum(numeric_only=True)
channels_df['rebal_out_30day'] = payments_df_30d_count.loc[chan_id].value
channels_df['amt_rebal_out_30day'] = int(payments_df_30d_sum.loc[chan_id].value)
payments_df_7d = payments_df_30d.loc[payments_df_30d['creation_date'] >= filter_7day]
if payments_df_7d.shape[0] > 0:
payments_df_7d_count = payments_df_7d.groupby('chan_out', as_index=True).count()
payments_df_7d_sum = payments_df_7d.groupby('chan_out', as_index=True).sum(numeric_only=True)
channels_df['rebal_out_7day'] = payments_df_7d_count.loc[chan_id].value
channels_df['amt_rebal_out_7day'] = int(payments_df_7d_sum.loc[chan_id].value)
payments_df_1d = payments_df_7d.loc[payments_df_7d['creation_date'] >= filter_1day]
if payments_df_1d.shape[0] > 0:
payments_df_1d_count = payments_df_1d.groupby('chan_out', as_index=True).count()
payments_df_1d_sum = payments_df_1d.groupby('chan_out', as_index=True).sum(numeric_only=True)
channels_df['rebal_out_1day'] = payments_df_1d_count.loc[chan_id].value
channels_df['amt_rebal_out_1day'] = int(payments_df_1d_sum.loc[chan_id].value)
if invoices_df.shape[0]> 0:
invoices_df_30d = invoices_df.loc[invoices_df['settle_date'] >= filter_30day]
invoices_df_7d = invoices_df_30d.loc[invoices_df_30d['settle_date'] >= filter_7day]
invoices_df_1d = invoices_df_7d.loc[invoices_df_7d['settle_date'] >= filter_1day]
invoices_df_count = DataFrame() if invoices_df.empty else invoices_df.groupby('chan_in', as_index=True).count()
invoices_df_30d_count = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True).count()
invoices_df_7d_count = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True).count()
invoices_df_1d_count = DataFrame() if invoices_df_1d.empty else invoices_df_1d.groupby('chan_in', as_index=True).count()
invoices_df_sum = DataFrame() if invoices_df.empty else invoices_df.groupby('chan_in', as_index=True).sum(numeric_only=True)
invoices_df_30d_sum = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True).sum(numeric_only=True)
invoices_df_7d_sum = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True).sum(numeric_only=True)
invoices_df_1d_sum = DataFrame() if invoices_df_1d.empty else invoices_df_1d.groupby('chan_in', as_index=True).sum(numeric_only=True)
channels_df['rebal_in'] = invoices_df_count.loc[chan_id].amt_paid if invoices_df_count.empty == False else 0
channels_df['rebal_in_30day'] = invoices_df_30d_count.loc[chan_id].amt_paid if invoices_df_30d_count.empty == False else 0
channels_df['rebal_in_7day'] = invoices_df_7d_count.loc[chan_id].amt_paid if invoices_df_7d_count.empty == False else 0
channels_df['rebal_in_1day'] = invoices_df_1d_count.loc[chan_id].amt_paid if invoices_df_1d_count.empty == False else 0
channels_df['amt_rebal_in'] = int(invoices_df_sum.loc[chan_id].amt_paid) if invoices_df_count.empty == False else 0
channels_df['amt_rebal_in_30day'] = int(invoices_df_30d_sum.loc[chan_id].amt_paid) if invoices_df_30d_count.empty == False else 0
channels_df['amt_rebal_in_7day'] = int(invoices_df_7d_sum.loc[chan_id].amt_paid) if invoices_df_7d_count.empty == False else 0
channels_df['amt_rebal_in_1day'] = int(invoices_df_1d_sum.loc[chan_id].amt_paid) if invoices_df_1d_count.empty == False else 0
rebal_payments_df = DataFrame.from_records(rebal_payments.filter(value__gte=1000).values())
if rebal_payments_df.shape[0] > 0:
invoice_hashes = DataFrame() if invoices_df.empty else invoices_df.loc[invoices_df['value'] >= 1000].groupby('chan_in', as_index=True)['r_hash'].apply(list)
invoice_hashes_30d = DataFrame() if invoices_df_30d.empty else invoices_df_30d.loc[invoices_df_30d['value'] >= 1000].groupby('chan_in', as_index=True)['r_hash'].apply(list)
invoice_hashes_7d = DataFrame() if invoices_df_7d.empty else invoices_df_7d.loc[invoices_df_7d['value'] >= 1000].groupby('chan_in', as_index=True)['r_hash'].apply(list)
invoice_hashes_1d = DataFrame() if invoices_df_1d.empty else invoices_df_1d.loc[invoices_df_1d['value'] >= 1000].groupby('chan_in', as_index=True)['r_hash'].apply(list)
channels_df['costs'] = 0 if channels_df['rebal_in'][0] == 0 or invoice_hashes.empty == True else int(rebal_payments_df.set_index('payment_hash', inplace=False).loc[invoice_hashes[chan_id]]['fee'].sum())
channels_df['costs_30day'] = 0 if channels_df['rebal_in_30day'][0] == 0 or invoice_hashes_30d.empty == True else int(rebal_payments_df.set_index('payment_hash', inplace=False).loc[invoice_hashes_30d[chan_id]]['fee'].sum())
channels_df['costs_7day'] = 0 if channels_df['rebal_in_7day'][0] == 0 or invoice_hashes_7d.empty == True else int(rebal_payments_df.set_index('payment_hash', inplace=False).loc[invoice_hashes_7d[chan_id]]['fee'].sum())
channels_df['costs_1day'] = 0 if channels_df['rebal_in_1day'][0] == 0 or invoice_hashes_1d.empty == True else int(rebal_payments_df.set_index('payment_hash', inplace=False).loc[invoice_hashes_1d[chan_id]]['fee'].sum())
channels_df['costs'] += Closures.objects.filter(funding_txid=channels_df['funding_txid'][0],funding_index=channels_df['output_index'][0])[0].closing_costs if Closures.objects.filter(funding_txid=channels_df['funding_txid'][0],funding_index=channels_df['output_index'][0]).exists() else 0
channels_df['profits'] = channels_df['revenue'] - channels_df['costs']
channels_df['profits_30day'] = channels_df['revenue_30day'] - channels_df['costs_30day']
channels_df['profits_7day'] = channels_df['revenue_7day'] - channels_df['costs_7day']
channels_df['profits_1day'] = channels_df['revenue_1day'] - channels_df['costs_1day']
channels_df['profits_vol'] = 0 if channels_df['amt_routed_out'].iloc[0] == 0 else (channels_df['profits']/(channels_df['amt_routed_out']/1000000)).astype(int)
channels_df['profits_vol_30day'] = 0 if channels_df['amt_routed_out_30day'].iloc[0] == 0 else (channels_df['profits_30day']/(channels_df['amt_routed_out_30day']/1000000)).astype(int)
channels_df['profits_vol_7day'] = 0 if channels_df['amt_routed_out_7day'].iloc[0] == 0 else (channels_df['profits_7day']/(channels_df['amt_routed_out_7day']/1000000)).astype(int)
channels_df['profits_vol_1day'] = 0 if channels_df['amt_routed_out_1day'].iloc[0] == 0 else (channels_df['profits_1day']/(channels_df['amt_routed_out_1day']/1000000)).astype(int)
channels_df = channels_df.copy()
channels_df['out_rate'] = int(((channels_df['revenue_7day_fees']/channels_df['amt_routed_out_7day_fees'])*1000000).iloc[0]) if channels_df['amt_routed_out_7day_fees'].iloc[0] > 0 else 0
channels_df['rebal_ppm'] = int(((channels_df['costs_7day']/channels_df['amt_rebal_in_7day'])*1000000).iloc[0]) if channels_df['amt_rebal_in_7day'].iloc[0] > 0 else 0
channels_df['assisted_ratio'] = round((channels_df['revenue_assist_7day_fees'] if channels_df['revenue_7day_fees'].iloc[0] == 0 else channels_df['revenue_assist_7day_fees']/channels_df['revenue_7day_fees']), 2)
channels_df['inbound_can'] = ((channels_df['remote_balance']*100)/channels_df['capacity'])/channels_df['ar_in_target']
channels_df['fee_ratio'] = 100 if channels_df['local_fee_rate'].iloc[0] == 0 else (round((channels_df['remote_fee_rate']/channels_df['local_fee_rate'])*1000/10)).astype(int)
channels_df['fee_check'] = 1 if channels_df['ar_max_cost'].iloc[0] == 0 else (((channels_df['fee_ratio']/channels_df['ar_max_cost'])*1000)/10).round(0).astype(int)
channels_df = channels_df.copy()
channels_df['steps'] = 0 if channels_df['inbound_can'].iloc[0] < 1 else (((channels_df['in_percent']-channels_df['ar_in_target'])/((channels_df['ar_amt_target']/channels_df['capacity'])*100))+0.999).astype(int)
channels_df['apy'] = 0.0
channels_df['apy_30day'] = 0.0
channels_df['apy_7day'] = 0.0
channels_df['apy_1day'] = 0.0
channels_df['assisted_apy'] = 0.0
channels_df['assisted_apy_30day'] = 0.0
channels_df['assisted_apy_7day'] = 0.0
channels_df['assisted_apy_1day'] = 0.0
channels_df['cv'] = 0.0
channels_df['cv_30day'] = 0.0
channels_df['cv_7day'] = 0.0
channels_df['cv_1day'] = 0.0
if node_capacity > 0:
outbound_ratio = node_outbound/node_capacity
if start_date is not None:
time_delta = datetime.now() - start_date.to_pydatetime()
days_routing = time_delta.days + (time_delta.seconds/86400)
channels_df['apy'] = round(((channels_df['profits']/days_routing)*36500)/(channels_df['capacity']*outbound_ratio), 2)
channels_df['assisted_apy'] = round(((channels_df['revenue_assist']/days_routing)*36500)/(channels_df['capacity']*(1-outbound_ratio)), 2)
channels_df['cv'] = round(((channels_df['revenue']/days_routing)*36500)/(channels_df['capacity']*outbound_ratio) + channels_df['assisted_apy'], 2)
channels_df['apy_30day'] = round((channels_df['profits_30day']*1216.6667)/(channels_df['capacity']*outbound_ratio), 2)
channels_df['apy_7day'] = round((channels_df['profits_7day']*5214.2857)/(channels_df['capacity']*outbound_ratio), 2)
channels_df['apy_1day'] = round((channels_df['profits_1day']*36500)/(channels_df['capacity']*outbound_ratio), 2)
channels_df['assisted_apy_30day'] = round((channels_df['revenue_assist_30day']*1216.6667)/(channels_df['capacity']*(1-outbound_ratio)), 2)
channels_df['assisted_apy_7day'] = round((channels_df['revenue_assist_7day']*5214.2857)/(channels_df['capacity']*(1-outbound_ratio)), 2)
channels_df['assisted_apy_1day'] = round((channels_df['revenue_assist_1day']*36500)/(channels_df['capacity']*(1-outbound_ratio)), 2)
channels_df['cv_30day'] = round((channels_df['revenue_30day']*1216.6667)/(channels_df['capacity']*outbound_ratio) + channels_df['assisted_apy_30day'], 2)
channels_df['cv_7day'] = round((channels_df['revenue_7day']*5214.2857)/(channels_df['capacity']*outbound_ratio) + channels_df['assisted_apy_7day'], 2)
channels_df['cv_1day'] = round((channels_df['revenue_1day']*36500)/(channels_df['capacity']*outbound_ratio) + channels_df['assisted_apy_1day'], 2)
autofees_df = DataFrame.from_records(Autofees.objects.filter(chan_id=chan_id).filter(timestamp__gte=filter_30day).order_by('-id').values())
if autofees_df.shape[0]> 0:
autofees_df['change'] = autofees_df.apply(lambda row: 0 if row.old_value == 0 else round((row.new_value-row.old_value)*100/row.old_value, 1), axis=1)
results_df = af.main(channel)
channels_df['new_rate'] = results_df[results_df['chan_id']==chan_id]['new_rate']
channels_df['adjustment'] = results_df[results_df['chan_id']==chan_id]['adjustment']
channels_df['net_routed_7day'] = results_df[results_df['chan_id']==chan_id]['net_routed_7day']
else:
channels_df = DataFrame()
payments_df = DataFrame()
invoices_df = DataFrame()
peer_info_df = DataFrame()
autofees_df = DataFrame()
context = {
'chan_id': chan_id,
'channel': [] if channels_df.empty else channels_df.to_dict(orient='records')[0],
'incoming_htlcs': PendingHTLCs.objects.filter(chan_id=chan_id).filter(incoming=True).order_by('hash_lock'),
'outgoing_htlcs': PendingHTLCs.objects.filter(chan_id=chan_id).filter(incoming=False).order_by('hash_lock'),
'peer_info': [] if peer_info_df.empty else peer_info_df.to_dict(orient='records')[0],
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links(),
'network_links': network_links(),
'autofees': [] if autofees_df.empty else autofees_df.to_dict(orient='records'),
}
try:
return render(request, 'channel.html', context)
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def opens(request):
if request.method == 'GET':
stub = lnrpc.LightningStub(lnd_connect())
self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey
current_peers = Channels.objects.filter(is_open=True).values_list('remote_pubkey')
exlcude_list = AvoidNodes.objects.values_list('pubkey')
filter_60day = datetime.now() - timedelta(days=60)
payments_60day = Payments.objects.filter(creation_date__gte=filter_60day, status=2).values_list('payment_hash')
open_list = PaymentHops.objects.filter(payment_hash__in=payments_60day).exclude(node_pubkey=self_pubkey).exclude(node_pubkey__in=current_peers).exclude(node_pubkey__in=exlcude_list).values('node_pubkey').annotate(ppm=(Sum('fee')/Sum('amt'))*1000000, score=Round((Round(Count('id')/1, output_field=IntegerField())+Round(Sum('amt')/100000, output_field=IntegerField()))/10, output_field=IntegerField()), count=Count('id'), amount=Sum('amt'), fees=Sum('fee'), sum_cost_to=Sum('cost_to')/(Sum('amt')/1000000), alias=Max('alias')).exclude(score=0).order_by('-score', 'ppm')[:21]
context = {
'open_list': open_list,
'avoid_list': AvoidNodes.objects.all(),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links()
}
return render(request, 'open_list.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def unprofitable_channels(request):
if request.method == 'GET':
# Get the timeframe from the request, default to 30 days
timeframe = request.GET.get('timeframe', '30')
timeframe_options = {
'1': {'days': 1, 'name': '1 Day'},
'7': {'days': 7, 'name': '7 Days'},
'30': {'days': 30, 'name': '30 Days'},
'90': {'days': 90, 'name': '90 Days'}
}
selected_timeframe = timeframe_options.get(timeframe, timeframe_options['30'])
filter_date = datetime.now() - timedelta(days=selected_timeframe['days'])
# Get active channels in a single query
channels = Channels.objects.filter(is_active=True, is_open=True)
channel_ids = [c.chan_id for c in channels]
# Get current block height once
try:
stub = lnrpc.LightningStub(lnd_connect())
current_block_height = stub.GetInfo(ln.GetInfoRequest()).block_height
except Exception as e:
logger.error(f'Error getting current block height: {str(e)}')
current_block_height = 0
VERY_NEW_DAYS = 7
ESTABLISHED_DAYS = 30
MAX_AGE_BONUS = 2.0
channel_metrics_dict = {chan_id: {
'routed_out_sats': 0,
'last_routing': None,
'last_routing_amount': 0,
'rebalanced_out_sats': 0, # For display of rebalances OUT
'last_rebalance': None, # For display of last rebalance OUT
'last_rebalance_amount': 0, # For display of last rebalance OUT amount
'fee_revenue': 0,
'rebalance_fee_cost': 0, # THIS WILL BE THE CORRECTED COST FOR PROFIT
'assisted_revenue': 0
} for chan_id in channel_ids}
# 1. Get outbound forwarding stats (routing out) - Populates 'fee_revenue'
out_forwards = Forwards.objects.filter(
chan_id_out__in=channel_ids,
forward_date__gte=filter_date
)
for forward in out_forwards:
chan_id = forward.chan_id_out
metrics = channel_metrics_dict[chan_id]
metrics['routed_out_sats'] += int(forward.amt_out_msat / 1000) if forward.amt_out_msat else 0
metrics['fee_revenue'] += forward.fee if forward.fee else 0
if metrics['last_routing'] is None or forward.forward_date > metrics['last_routing']:
metrics['last_routing'] = forward.forward_date
metrics['last_routing_amount'] = int(forward.amt_out_msat / 1000) if forward.amt_out_msat else 0
# 2. Get inbound forwarding stats (assisted revenue) - Populates 'assisted_revenue'
in_forwards = Forwards.objects.filter(
chan_id_in__in=channel_ids,
forward_date__gte=filter_date
)
for forward in in_forwards:
chan_id = forward.chan_id_in
channel_metrics_dict[chan_id]['assisted_revenue'] += forward.fee if forward.fee else 0
# --- Calculate rebalance_fee_cost for PROFIT (cost of rebalances INTO the channel) ---
# This section calculates the cost component for the profit metric accurately.
all_rebalance_payments_in_period = Payments.objects.filter(
creation_date__gte=filter_date,
status=2,
rebal_chan__isnull=False
)
payment_fees_by_hash = {p.payment_hash: p.fee for p in all_rebalance_payments_in_period}
rebalance_invoices_into_our_channels = Invoices.objects.filter(
chan_in__in=channel_ids,
state=1,
settle_date__gte=filter_date,
r_hash__in=all_rebalance_payments_in_period.values_list('payment_hash', flat=True)
)
for invoice in rebalance_invoices_into_our_channels:
chan_id_receiving_rebalance = invoice.chan_in
if chan_id_receiving_rebalance in channel_metrics_dict:
cost_of_this_rebalance_in = payment_fees_by_hash.get(invoice.r_hash, 0)
channel_metrics_dict[chan_id_receiving_rebalance]['rebalance_fee_cost'] += cost_of_this_rebalance_in
# --- End of rebalance_fee_cost calculation for profit ---
# 3. Get outbound rebalance data FOR DISPLAY PURPOSES (rebalanced_out_sats, last_rebalance)
# This data is for display columns like "Rebalanced Out Sats" and "Last Rebalance (Out)".
# It does NOT contribute to the 'rebalance_fee_cost' used in profit calculation.
rebalance_payments_out_for_display = Payments.objects.filter(
chan_out__in=channel_ids,
creation_date__gte=filter_date,
status=2,
rebal_chan__isnull=False
)
for payment in rebalance_payments_out_for_display:
chan_id = payment.chan_out
if chan_id in channel_metrics_dict: # Ensure channel is in our scope
metrics = channel_metrics_dict[chan_id]
metrics['rebalanced_out_sats'] += payment.value if payment.value else 0
# Update last_rebalance and last_rebalance_amount if this payment is later
if metrics['last_rebalance'] is None or payment.creation_date > metrics['last_rebalance']:
metrics['last_rebalance'] = payment.creation_date
metrics['last_rebalance_amount'] = payment.value if payment.value else 0
# Calculate normalized metrics for each channel
outbound_activities = []
outbound_ratios = [] # NEW: Track outbound activity as a ratio of channel capacity
assisted_revenues = []
fee_rates = []
for chan_id, metrics_data in channel_metrics_dict.items(): # Changed metrics to metrics_data to avoid conflict
# Get channel object to access capacity
channel_obj = next((c for c in channels if c.chan_id == chan_id), None)
if channel_obj:
total_outbound = metrics_data['routed_out_sats'] + metrics_data['rebalanced_out_sats']
outbound_activities.append(total_outbound)
# Calculate outbound ratio (outbound activity / channel capacity)
capacity = channel_obj.capacity or 1 # Avoid division by zero
outbound_ratio = total_outbound / capacity
outbound_ratios.append(outbound_ratio)
assisted_revenues.append(metrics_data['assisted_revenue'])
fee_rate = getattr(channel_obj, 'local_fee_rate', 0) or 0
fee_rates.append(fee_rate)
# Find min/max values for normalization
max_outbound = max(outbound_activities) if outbound_activities else 1
max_outbound_ratio = max(outbound_ratios) if outbound_ratios else 1
max_assisted = max(assisted_revenues) if assisted_revenues else 1
max_fee_rate = max(fee_rates) if fee_rates else 1000
# Thresholds for high/medium/low classifications
# NEW: Use ratio-based thresholds for activity
high_ratio_threshold = max(max_outbound_ratio * 0.7, 0.5) # At least 50% of capacity or 70% of max ratio
medium_ratio_threshold = max(max_outbound_ratio * 0.3, 0.2) # At least 20% of capacity or 30% of max ratio
high_assisted_threshold = max_assisted * 0.7
medium_assisted_threshold = max_assisted * 0.3
high_fee_threshold = max(max_fee_rate, 1000) * 0.7
medium_fee_threshold = max(max_fee_rate, 1000) * 0.3
# Build the final channel metrics list
channel_metrics = []
for channel in channels:
chan_id = channel.chan_id
metrics = channel_metrics_dict[chan_id] # Use 'metrics' here as it's conventional in this loop
# Calculate profit (fee revenue - rebalance costs)
# THIS NOW USES THE CORRECTED 'rebalance_fee_cost'
profit = metrics['fee_revenue'] - metrics['rebalance_fee_cost']
# Format last routing and rebalance for display
last_routing_display = { # Renamed to avoid conflict if 'last_routing' is a direct key
'date': metrics['last_routing'],
'amount': metrics['last_routing_amount']
} if metrics['last_routing'] else None
last_rebalance_display = { # Renamed
'date': metrics['last_rebalance'], # This is last rebalance OUT
'amount': metrics['last_rebalance_amount'] # This is last rebalance OUT amount
} if metrics['last_rebalance'] else None
# Calculate local ratio (percentage of capacity that is local balance)
local_ratio = channel.local_balance / channel.capacity if channel.capacity > 0 else 0
local_ratio_pct = round(local_ratio * 100, 2)
# Get activity metrics
routing_out = metrics['routed_out_sats']
rebalancing_out = metrics['rebalanced_out_sats']
total_outbound = routing_out + rebalancing_out
assisted_revenue = metrics['assisted_revenue']
# NEW: Calculate outbound ratio (outbound activity / channel capacity)
outbound_ratio = total_outbound / channel.capacity if channel.capacity > 0 else 0
# Get fee rate
local_fee_rate = getattr(channel, 'local_fee_rate', 0) or 0
# Calculate priority score based on activity and fees
# Lower score = better (less stuck), higher score = worse (more stuck)
# We'll use a score from 0-7 matching your priority list
# Start with worst score
priority_score = 7.0
# Check conditions using ratio-based thresholds
if outbound_ratio > high_ratio_threshold and local_fee_rate > high_fee_threshold:
# 1) High routing out ratio with high local_fee_rate
priority_score = 1.0
elif outbound_ratio > high_ratio_threshold and assisted_revenue > high_assisted_threshold:
# 2) High rebalancing out ratio with high assisted revenue
priority_score = 2.0
elif outbound_ratio > medium_ratio_threshold and local_fee_rate > medium_fee_threshold:
# 3) Medium routing out ratio with medium local_fee_rate
priority_score = 3.0
elif outbound_ratio > medium_ratio_threshold and assisted_revenue > medium_assisted_threshold:
# 4) Medium rebalancing out ratio with medium assisted revenue
priority_score = 4.0
elif outbound_ratio > 0 and local_fee_rate > 0:
# 5) Low routing out ratio and low local_fee_rate
priority_score = 5.0
elif outbound_ratio > 0 and assisted_revenue > 0:
# 6) Low rebalancing out ratio and low assisted revenue
priority_score = 6.0
# else: 7) No activity and no compensation (already set as default)
# Apply assisted revenue bonus
if assisted_revenue > high_assisted_threshold:
# Significant bonus for high assisted revenue
assisted_bonus = 1.5
priority_score = max(1.0, priority_score - assisted_bonus)
elif assisted_revenue > medium_assisted_threshold:
# Moderate bonus for medium assisted revenue
assisted_bonus = 0.75
priority_score = max(1.0, priority_score - assisted_bonus)
elif assisted_revenue > 0:
# Small bonus for any assisted revenue
assisted_bonus = 0.25
priority_score = max(1.0, priority_score - assisted_bonus)
# Apply local liquidity adjustment
# If local_ratio is low, reduce the penalty (improve the score)
low_local_threshold = 0.2 # 20% or less is considered low local liquidity
high_local_threshold = 0.8 # 80% or more is considered high local liquidity
if local_ratio <= low_local_threshold:
# Significant improvement for channels with very low local liquidity
local_liquidity_adjustment = 3.0 * (1 - (local_ratio / low_local_threshold))
priority_score = max(1.0, priority_score - local_liquidity_adjustment)
elif local_ratio >= high_local_threshold:
# Slight penalty for channels with very high local liquidity
local_liquidity_adjustment = 0.5 * ((local_ratio - high_local_threshold) / (1 - high_local_threshold))
priority_score = min(7.0, priority_score + local_liquidity_adjustment)
age_bonus = 0.0
if current_block_height > 0 and channel.chan_id: # Ensure we have data
try:
opening_block_height = int(channel.chan_id) >> 40
if opening_block_height > 0: # Ensure valid opening height
channel_age_blocks = max(0, current_block_height - opening_block_height)
channel_age_days = channel_age_blocks / 144 # Approx days
if channel_age_days < VERY_NEW_DAYS:
# Max bonus for very new channels
age_bonus = MAX_AGE_BONUS
elif channel_age_days < ESTABLISHED_DAYS:
# Linearly decreasing bonus for channels between 7 and 30 days
age_progress = (channel_age_days - VERY_NEW_DAYS) / (ESTABLISHED_DAYS - VERY_NEW_DAYS)
age_bonus = MAX_AGE_BONUS * (1 - age_progress)
# else: age_bonus remains 0 for established channels
except Exception as e:
# Handle potential errors during age calculation gracefully
logger.error(f'Error calculating age for chan_id {channel.chan_id}: {str(e)}')
channel_age_days = -1 # Indicate unknown age
else:
channel_age_days = -1 # Indicate unknown age if block height or chan_id missing
# Apply the age bonus, ensuring score doesn't go below 1.0
priority_score = max(1.0, priority_score - age_bonus)
# Normalize to 0-1 scale (divide by 7)
smart_stuck_index = round(priority_score / 7.0, 2)
# Add metrics to the list
channel_metrics.append({
'chan_id': chan_id,
'alias': channel.alias or "---",
'routed_out_sats': routing_out,
'last_routing': last_routing_display,
'rebalanced_out_sats': rebalancing_out,
'last_rebalance': last_rebalance_display,
'profit': profit,
'assisted_revenue': assisted_revenue,
'initiator': channel.initiator,
'capacity': channel.capacity,
'capacity_millions': round(channel.capacity / 1000000, 1),
'local_balance': channel.local_balance,
'remote_balance': channel.remote_balance,
'local_ratio': local_ratio_pct,
'stuck_index': smart_stuck_index,
'local_fee_rate': local_fee_rate,
'outbound_ratio': round(outbound_ratio * 100, 2), # Add this to show the ratio as percentage
'priority_rank': round(priority_score, 1), # Shows the exact ranking with adjustment
'age_days': round(channel_age_days) if channel_age_days >= 0 else 'N/A' # Add age for display
})
# Sort by profit (ascending to show least profitable first)
channel_metrics.sort(key=lambda x: x['profit'])
context = {
'channels': channel_metrics,
'timeframe': timeframe,
'timeframe_name': selected_timeframe['name'],
'timeframe_options': timeframe_options
}
return render(request, 'unprofitable_channels.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def actions(request):
if request.method == 'GET':
channels = Channels.objects.filter(is_active=True, is_open=True, private=False).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*1000)/Sum('capacity'), inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*1000)/Sum('capacity'))
filter_7day = datetime.now() - timedelta(days=7)
forwards = Forwards.objects.filter(forward_date__gte=filter_7day)
action_list = []
for channel in channels:
result = {}
result['chan_id'] = channel.chan_id
result['short_chan_id'] = channel.short_chan_id
result['remote_pubkey'] = channel.remote_pubkey
result['alias'] = channel.alias
result['capacity'] = channel.capacity
result['local_balance'] = channel.local_balance + channel.pending_outbound
result['remote_balance'] = channel.remote_balance + channel.pending_inbound
result['outbound_percent'] = int(round(channel.outbound_percent/10, 0))
result['inbound_percent'] = int(round(channel.inbound_percent/10, 0))
result['unsettled_balance'] = channel.unsettled_balance
result['local_base_fee'] = channel.local_base_fee
result['local_fee_rate'] = channel.local_fee_rate
result['remote_base_fee'] = channel.remote_base_fee
result['remote_fee_rate'] = channel.remote_fee_rate
result['routed_in_7day'] = forwards.filter(chan_id_in=channel.chan_id).count()
result['routed_out_7day'] = forwards.filter(chan_id_out=channel.chan_id).count()
result['i7D'] = 0 if result['routed_in_7day'] == 0 else int(forwards.filter(chan_id_in=channel.chan_id).aggregate(Sum('amt_in_msat'))['amt_in_msat__sum']/10000000)/100
result['o7D'] = 0 if result['routed_out_7day'] == 0 else int(forwards.filter(chan_id_out=channel.chan_id).aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/10000000)/100
result['auto_rebalance'] = channel.auto_rebalance
result['ar_target'] = channel.ar_in_target
if result['o7D'] > (result['i7D']*1.10) and result['outbound_percent'] > 75:
logger.debug('Auto-Enable Case 1: Pass')
elif result['o7D'] > (result['i7D']*1.10) and result['inbound_percent'] > 75 and channel.auto_rebalance == False:
if channel.local_fee_rate <= channel.remote_fee_rate:
logger.debug('Case 6: Peer Fee Too High')
result['output'] = 'Peer Fee Too High'
result['reason'] = 'o7D > i7D AND Inbound Liq > 75% AND Local Fee < Remote Fee'
continue
logger.debug('Case 2: Enable AR - o7D > i7D AND Inbound Liq > 75%')
result['output'] = 'Enable AR'
result['reason'] = 'o7D > i7D AND Inbound Liq > 75%'
elif result['o7D'] < (result['i7D']*1.10) and result['outbound_percent'] > 75 and channel.auto_rebalance == True:
logger.debug('Case 3: Disable AR - o7D < i7D AND Outbound Liq > 75%')
result['output'] = 'Disable AR'
result['reason'] = 'o7D < i7D AND Outbound Liq > 75%'
elif result['o7D'] < (result['i7D']*1.10) and result['inbound_percent'] > 75:
logger.debug('Case 4: Pass')
continue
else:
logger.debug('Case 5: Pass')
continue
if len(result) > 0:
action_list.append(result)
context = {
'action_list': action_list,
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links(),
'network_links': network_links()
}
return render(request, 'action_list.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def pending_htlcs(request):
if request.method == 'GET':
stub = lnrpc.LightningStub(lnd_connect())
block_height = stub.GetInfo(ln.GetInfoRequest()).block_height
context = {
'incoming_htlcs': PendingHTLCs.objects.filter(incoming=True).annotate(blocks_til_expiration=Sum('expiration_height')-block_height, hours_til_expiration=((Sum('expiration_height')-block_height)*10)/60).order_by('expiration_height'),
'outgoing_htlcs': PendingHTLCs.objects.filter(incoming=False).annotate(blocks_til_expiration=Sum('expiration_height')-block_height, hours_til_expiration=((Sum('expiration_height')-block_height)*10)/60).order_by('expiration_height')
}
return render(request, 'pending_htlcs.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def failed_htlcs(request):
if request.method == 'GET':
try:
filter_7day = datetime.now() - timedelta(days=7)
agg_failed_htlcs = FailedHTLCs.objects.filter(timestamp__gte=filter_7day, wire_failure=99).values('chan_id_in', 'chan_id_out').annotate(count=Count('id'), volume=Sum('amount'), chan_in_alias=Max('chan_in_alias'), chan_out_alias=Max('chan_out_alias')).order_by('-count')[:21]
context = {
'agg_failed_htlcs': agg_failed_htlcs
}
return render(request, 'failed_htlcs.html', context)
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def payments(request):
if request.method == 'GET':
context = {
'payments': Payments.objects.exclude(status=3).annotate(ppm=Round((Sum('fee')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-creation_date')[:150],
}
return render(request, 'payments.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def invoices(request):
if request.method == 'GET':
context = {
'invoices': Invoices.objects.filter(state=1).order_by('-creation_date')[:150],
}
return render(request, 'invoices.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def rebalances(request):
if request.method == 'GET':
try:
return render(request, 'rebalances.html')
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def batch(request):
if request.method == 'GET':
stub = lnrpc.LightningStub(lnd_connect())
context = {
'iterator': range(1,11),
'balances': stub.WalletBalance(ln.WalletBalanceRequest())
}
return render(request, 'batch.html', context)
else:
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def trades(request):
if request.method == 'GET':
stub = lnrpc.LightningStub(lnd_connect())
context = {
'trade_link': create_trade_details(stub)
}
return render(request, 'trades.html', context)
else:
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def reset(request):
if request.method == 'GET':
context = {
'tables':[
{'name':'Forwards', 'count': Forwards.objects.count()},
{'name':'Payments', 'count': Payments.objects.count()},
{'name':'PaymentHops', 'count': PaymentHops.objects.count()},
{'name':'Invoices', 'count': Invoices.objects.count()},
{'name':'Rebalancer', 'count': Rebalancer.objects.count()},
{'name':'Closures', 'count': Closures.objects.count()},
{'name':'Resolutions', 'count': Resolutions.objects.count()},
{'name':'Peers', 'count': Peers.objects.count()},
{'name':'Channels', 'count': Channels.objects.count()},
{'name':'PendingChannels', 'count': PendingChannels.objects.count()},
{'name':'Onchain', 'count': Onchain.objects.count()},
{'name':'PendingHTLCs', 'count': PendingHTLCs.objects.count()},
{'name':'FailedHTLCs', 'count': FailedHTLCs.objects.count()},
{'name':'HistFailedHTLC', 'count': HistFailedHTLC.objects.count()},
{'name':'Autopilot', 'count': Autopilot.objects.count()},
{'name':'Autofees', 'count': Autofees.objects.count()},
{'name':'AvoidNodes', 'count': AvoidNodes.objects.count()},
{'name':'PeerEvents', 'count': PeerEvents.objects.count()},
{'name':'LocalSettings', 'count': LocalSettings.objects.count()}
]
}
return render(request, 'reset.html', context)
else:
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def addresses(request):
if request.method == 'GET':
try:
stub = walletstub.WalletKitStub(lnd_connect())
address_data = stub.ListAddresses(walletrpc.ListAddressesRequest())
context = {
'address_data': address_data,
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'network_links': network_links()
}
return render(request, 'addresses.html', context)
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect(request.META.get('HTTP_REFERER'))
def open_peer(peer_pubkey, stub):
if Peers.objects.filter(pubkey=peer_pubkey, connected=True).exists():
return True
else:
try:
node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node
host = node.addresses[0].addr
ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host)
stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr))
return True
except:
return False
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def batch_open(request):
if request.method == 'POST':
form = BatchOpenForm(request.POST)
if form.is_valid():
count = 0
fail = False
open_list = []
stub = lnrpc.LightningStub(lnd_connect())
if form.cleaned_data['pubkey1'] and form.cleaned_data['amt1'] and len(form.cleaned_data['pubkey1']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey1']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':peer_pubkey, 'amt':form.cleaned_data['amt1']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 1!')
if form.cleaned_data['pubkey2'] and form.cleaned_data['amt2'] and len(form.cleaned_data['pubkey2']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey2']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey2'], 'amt':form.cleaned_data['amt2']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 2!')
if form.cleaned_data['pubkey3'] and form.cleaned_data['amt3'] and len(form.cleaned_data['pubkey3']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey3']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey3'], 'amt':form.cleaned_data['amt3']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 3!')
if form.cleaned_data['pubkey4'] and form.cleaned_data['amt4'] and len(form.cleaned_data['pubkey4']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey4']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey4'], 'amt':form.cleaned_data['amt4']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 4!')
if form.cleaned_data['pubkey5'] and form.cleaned_data['amt5'] and len(form.cleaned_data['pubkey5']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey5']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey5'], 'amt':form.cleaned_data['amt5']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 5!')
if form.cleaned_data['pubkey6'] and form.cleaned_data['amt6'] and len(form.cleaned_data['pubkey6']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey6']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey6'], 'amt':form.cleaned_data['amt6']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 6!')
if form.cleaned_data['pubkey7'] and form.cleaned_data['amt7'] and len(form.cleaned_data['pubkey7']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey7']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey7'], 'amt':form.cleaned_data['amt7']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 7!')
if form.cleaned_data['pubkey8'] and form.cleaned_data['amt8'] and len(form.cleaned_data['pubkey8']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey8']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey8'], 'amt':form.cleaned_data['amt8']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 8!')
if form.cleaned_data['pubkey9'] and form.cleaned_data['amt9'] and len(form.cleaned_data['pubkey9']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey9']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey9'], 'amt':form.cleaned_data['amt9']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 9!')
if form.cleaned_data['pubkey10'] and form.cleaned_data['amt10'] and len(form.cleaned_data['pubkey10']) == 66:
count += 1
peer_pubkey = form.cleaned_data['pubkey10']
connected = open_peer(peer_pubkey, stub)
if connected:
open_list.append({'pubkey':form.cleaned_data['pubkey10'], 'amt':form.cleaned_data['amt10']})
else:
fail = True
messages.error(request, 'Unable to connect with peer 10!')
if fail == True:
return redirect('batch')
if len (open_list) > 0:
try:
channels = []
for open in open_list:
channel_open = ln.BatchOpenChannel()
channel_open.node_pubkey = bytes.fromhex(open['pubkey'])
channel_open.local_funding_amount = open['amt']
channels.append(channel_open)
response = stub.BatchOpenChannel(ln.BatchOpenChannelRequest(channels=channels, sat_per_vbyte=form.cleaned_data['fee_rate']))
logger.debug(f'Batch open response: {response}')
messages.success(request, 'Batch opened channels!')
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]
messages.error(request, 'Batch open failed! Error: ' + error_msg)
else:
messages.error(request, 'No channels specified!')
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect('batch')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def forwards(request):
if request.method == 'GET':
return render(request, 'forwards.html')
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def rebalancing(request):
if request.method == 'GET':
context = {
'local_settings': get_local_settings('AR-'),
'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '',
'graph_links': graph_links()
}
return render(request, 'rebalancing.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def keysends(request):
if request.method == 'GET':
context = {
'keysends': Invoices.objects.filter(keysend_preimage__isnull=False).order_by('-settle_date')
}
return render(request, 'keysends.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def autopilot(request):
if request.method == 'GET':
chan_id = request.GET.urlencode()[1:]
filter_21d = datetime.now() - timedelta(days=21)
autopilot = Autopilot.objects.filter(timestamp__gte=filter_21d).order_by('-id') if chan_id == "" else Autopilot.objects.filter(chan_id = chan_id).filter(timestamp__gte=filter_21d).order_by('-id')
context = {
'autopilot': autopilot
}
return render(request, 'autopilot.html', context)
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def outbound_fee_log(request):
if request.method == 'GET':
try:
chan_id = request.GET.urlencode()[1:]
filter_7d = datetime.now() - timedelta(days=7)
outbound_fee_log_df = DataFrame.from_records(Autofees.objects.filter(timestamp__gte=filter_7d).order_by('-id').values() if chan_id == "" else Autofees.objects.filter(chan_id=chan_id).filter(timestamp__gte=filter_7d).order_by('-id').values())
if outbound_fee_log_df.shape[0]> 0:
outbound_fee_log_df['change'] = outbound_fee_log_df.apply(lambda row: 0 if row.old_value == 0 else round((row.new_value-row.old_value)*100/row.old_value, 1), axis=1)
context = {
'outbound_fee_log': [] if outbound_fee_log_df.empty else outbound_fee_log_df.to_dict(orient='records')
}
return render(request, 'outbound_fee_log.html', context)
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def inbound_fee_log(request):
if request.method == 'GET':
try:
chan_id = request.GET.urlencode()[1:]
filter_7d = datetime.now() - timedelta(days=7)
inbound_fee_log_df = DataFrame.from_records(InboundFeeLog.objects.filter(timestamp__gte=filter_7d).order_by('-id').values() if chan_id == "" else InboundFeeLog.objects.filter(chan_id=chan_id).filter(timestamp__gte=filter_7d).order_by('-id').values())
if inbound_fee_log_df.shape[0]> 0:
inbound_fee_log_df['change'] = inbound_fee_log_df.apply(lambda row: 0 if row.old_value == 0 else round((row.new_value-row.old_value)*100/row.old_value, 1), axis=1)
context = {
'inbound_fee_log': [] if inbound_fee_log_df.empty else inbound_fee_log_df.to_dict(orient='records')
}
return render(request, 'inbound_fee_log.html', context)
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def peerevents(request):
if request.method == 'GET':
try:
return render(request, 'peerevents.html')
except Exception as e:
try:
error = str(e.code())
except:
error = str(e)
return render(request, 'error.html', {'error': error})
else:
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def open_channel_form(request):
if request.method == 'POST':
form = OpenChannelForm(request.POST)
if form.is_valid():
try:
stub = lnrpc.LightningStub(lnd_connect())
peer_pubkey = form.cleaned_data['peer_pubkey']
connected = False
if Peers.objects.filter(pubkey=peer_pubkey, connected=True).exists():
connected = True
else:
try:
node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node
host = node.addresses[0].addr
ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host)
response = stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr))
connected = True
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]
messages.error(request, 'Error connecting to new peer: ' + error_msg)
if connected:
for response in stub.OpenChannel(ln.OpenChannelRequest(node_pubkey=bytes.fromhex(peer_pubkey), local_funding_amount=form.cleaned_data['local_amt'], sat_per_byte=form.cleaned_data['sat_per_byte'])):
messages.success(request, 'Channel created! Funding TXID: ' + str(response.chan_pending.txid[::-1].hex()) + ':' + str(response.chan_pending.output_index))
break
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]
messages.error(request, 'Channel creation failed! Error: ' + error_msg)
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def close_channel_form(request):
if request.method == 'POST':
form = CloseChannelForm(request.POST)
if form.is_valid():
try:
chan_id = form.cleaned_data['chan_id']
if chan_id.count('x') == 2 and len(chan_id) >= 5:
target_channel = Channels.objects.filter(short_chan_id=chan_id)
else:
target_channel = Channels.objects.filter(chan_id=chan_id)
if target_channel.exists():
target_channel = target_channel.get()
funding_txid = target_channel.funding_txid
output_index = target_channel.output_index
force_close = form.cleaned_data['force']
target_fee = form.cleaned_data['target_fee']
if not force_close and not target_fee:
messages.error(request, 'Expected a fee rate for graceful closure. Please try again.')
channel_point = ln.ChannelPoint()
channel_point.funding_txid_bytes = bytes.fromhex(funding_txid)
channel_point.funding_txid_str = funding_txid
channel_point.output_index = output_index
stub = lnrpc.LightningStub(lnd_connect())
if force_close:
for response in stub.CloseChannel(ln.CloseChannelRequest(channel_point=channel_point, force=True)):
messages.success(request, 'Channel force closed! Closing TXID: ' + str(response.close_pending.txid[::-1].hex()) + ':' + str(response.close_pending.output_index))
break
else:
for response in stub.CloseChannel(ln.CloseChannelRequest(channel_point=channel_point, sat_per_byte=target_fee)):
messages.success(request, 'Channel gracefully closed! Closing TXID: ' + str(response.close_pending.txid[::-1].hex()) + ':' + str(response.close_pending.output_index))
break
else:
messages.error(request, 'Channel ID is not valid. Please try again.')
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]
messages.error(request, 'Channel close failed! Error: ' + error_msg)
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def connect_peer_form(request):
if request.method == 'POST':
form = ConnectPeerForm(request.POST)
if form.is_valid():
try:
stub = lnrpc.LightningStub(lnd_connect())
peer_id = form.cleaned_data['peer_id']
if peer_id.count('@') == 0 and len(peer_id) == 66:
peer_pubkey = peer_id
node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node
host = node.addresses[0].addr
elif peer_id.count('@') == 1 and len(peer_id.split('@')[0]) == 66:
peer_pubkey, host = peer_id.split('@')
else:
raise Exception('Invalid peer pubkey or connection string.')
ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host)
response = stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr))
messages.success(request, 'Connection successful!' + str(response))
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]
messages.error(request, 'Connection request failed! Error: ' + error_msg)
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def add_invoice_form(request):
if request.method == 'POST':
form = AddInvoiceForm(request.POST)
if form.is_valid():
try:
stub = lnrpc.LightningStub(lnd_connect())
response = stub.AddInvoice(ln.Invoice(value=form.cleaned_data['value']))
messages.success(request, 'Invoice created! ' + str(response.payment_request))
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]
messages.error(request, 'Invoice creation failed! Error: ' + error_msg)
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect('home')
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def rebalance(request):
if request.method == 'POST':
form = RebalancerForm(request.POST)
if form.is_valid():
try:
if Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey']).exists() or form.cleaned_data['last_hop_pubkey'] == '':
chan_ids = [ch.chan_id for ch in form.cleaned_data['outgoing_chan_ids']]
if len(chan_ids) > 0:
if form.cleaned_data['last_hop_pubkey'] != '':
target_channel = Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey']).first()
target_alias = target_channel.alias if target_channel.alias != '' else target_channel.remote_pubkey[:12]
else:
target_alias = ''
fee_limit = round(form.cleaned_data['fee_limit']*form.cleaned_data['value']*0.000001, 3)
Rebalancer(value=form.cleaned_data['value'], fee_limit=fee_limit, outgoing_chan_ids=str(chan_ids).replace('\'', ''), last_hop_pubkey=form.cleaned_data['last_hop_pubkey'], target_alias=target_alias, duration=form.cleaned_data['duration'], manual=True).save()
messages.success(request, 'Rebalancer request created!')
else:
messages.error(request, 'You must select atleast one outgoing channel.')
else:
messages.error(request, 'Target peer is invalid or unknown.')
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]
messages.error(request, 'Error entering rebalancer request! Error: ' + error_msg)
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
def get_local_settings(*prefixes):
form = []
if 'AR-' in prefixes:
form.append({'unit': '', 'form_id': 'update_channels', 'id': 'update_channels'})
form.append({'unit': '', 'form_id': 'enabled', 'value': 0, 'label': 'AR Enabled', 'id': 'AR-Enabled', 'title':'This enables or disables the auto-scheduling function', 'min':0, 'max':1},)
form.append({'unit': '%', 'form_id': 'target_percent', 'value': 3.0, 'label': 'AR Target Amount', 'id': 'AR-Target%', 'title': 'The percentage of the total capacity to target as the rebalance amount. Default 3', 'min':0.1, 'max':100})
form.append({'unit': 'min', 'form_id': 'target_time', 'value': 5, 'label': 'AR Target Time', 'id': 'AR-Time', 'title': 'The time spent in minutes for each individual rebalance attempt. Default 5', 'min':1, 'max':60})
form.append({'unit': 'ppm', 'form_id': 'fee_rate', 'value': 500, 'label': 'AR Max Fee Rate', 'id': 'AR-MaxFeeRate', 'title': 'The max rate we can ever use to refill a channel with outbound. Default 500', 'min':1, 'max':5000})
form.append({'unit': '%', 'form_id': 'outbound_percent', 'value': 75, 'label': 'AR Target Out Above', 'id': 'AR-Outbound%', 'title': 'Default oTarget% for new channels. When a channel is not AR enabled; the oTarget% is the minimum outbound a channel must have to be a source for refilling another channel. Default 75', 'min':1, 'max':100})
form.append({'unit': '%', 'form_id': 'inbound_percent', 'value': 90, 'label': 'AR Target In Above', 'id': 'AR-Inbound%', 'title': 'Default iTarget% for new channels. When a channel is AR enabled; the iTarget% is the minimum inbound a channel must have before selected for auto rebalance. Default 90', 'min':1, 'max':100})
form.append({'unit': '%', 'form_id': 'max_cost', 'value': 65, 'label': 'AR Max Cost', 'id': 'AR-MaxCost%', 'title': 'The ppm to target which is the percentage of the outbound fee rate for the channel being refilled. Default 65', 'min':1, 'max':100})
form.append({'unit': '%', 'form_id': 'variance', 'value': 0, 'label': 'AR Variance', 'id': 'AR-Variance', 'title': 'The percentage of the target amount to be randomly varied with every rebalance attempt. Default 0', 'min':0, 'max':100})
form.append({'unit': 'min', 'form_id': 'wait_period', 'value': 30, 'label': 'AR Wait Period', 'id': 'AR-WaitPeriod', 'title': 'The minutes we should wait after a failed attempt before trying again. Default 30', 'min':1, 'max':10080})
form.append({'unit': '', 'form_id': 'autopilot', 'value': 0, 'label': 'Autopilot', 'id': 'AR-Autopilot', 'title': 'This enables or disables the Auto-Rebalance function for individual channels based on flow (automatically acts upon suggestions on this page: /actions)', 'min':0, 'max':1})
form.append({'unit': 'days', 'form_id': 'autopilotdays', 'value': 7, 'label': 'Autopilot Days', 'id': 'AR-APDays', 'title': 'Number of days to consider for autopilot calculations. Default 7', 'min':0, 'max':100})
form.append({'unit': '', 'form_id': 'workers', 'value': 1, 'label': 'Workers', 'id': 'AR-Workers', 'title': 'Number of concurrent rebalance workers to run at once (use a proper value for your hardware, this will increase the load on the lnd server). Default 1', 'min':1, 'max':12})
if 'AF-' in prefixes:
form.append({'unit': '', 'form_id': 'af_enabled', 'value': 0, 'label': 'Autofee', 'id': 'AF-Enabled', 'title': 'Enable/Disable All Auto-fee functionality', 'min':0, 'max':1})
form.append({'unit': '', 'form_id': 'af_inbound', 'value': 0, 'label': 'Inbound Fees', 'id': 'AF-InboundFees', 'title': 'Enable/Disable Inbound Auto-fee functionality', 'min':0, 'max':1})
form.append({'unit': 'ppm', 'form_id': 'af_maxRate', 'value': 2500, 'label': 'AF Max Rate', 'id': 'AF-MaxRate', 'title': 'Maximum Rate that can be adjusted to. Default 2500', 'min':0, 'max':5000})
form.append({'unit': 'ppm', 'form_id': 'af_minRate', 'value': 0, 'label': 'AF Min Rate', 'id': 'AF-MinRate', 'title': 'Minimum Rate that can be adjusted to. Default 0', 'min':0, 'max':5000})
form.append({'unit': 'ppm', 'form_id': 'af_increment', 'value': 5, 'label': 'AF Increment', 'id': 'AF-Increment', 'title': 'Target fee rate will always be a multiple of this value. Default 5', 'min':1, 'max':100})
form.append({'unit': 'x', 'form_id': 'af_multiplier', 'value': 5, 'label': 'AF Multiplier', 'id': 'AF-Multiplier', 'title': 'Multiplier to be applied to Auto-Fee adjustments. Default 5', 'min':1, 'max':100})
form.append({'unit': '', 'form_id': 'af_failedHTLCs', 'value': 25, 'label': 'AF FailedHTLCs', 'id': 'AF-FailedHTLCs', 'title': 'Failed HTLCs required since last fee update to trigger a fee increase (when chan liq% is below AR-LowLiq). Default 25', 'min':1, 'max':100})
form.append({'unit': 'hours', 'form_id': 'af_updateHours', 'value': 24, 'label': 'AF Update', 'id': 'AF-UpdateHours', 'title': 'Minimum number of hours between fee updates for an individual channel. Default 24', 'min':1, 'max':100})
form.append({'unit': '%', 'form_id': 'af_lowliq', 'value': 15, 'label': 'AF LowLiq', 'id': 'AF-LowLiqLimit', 'title': 'Limit for running low liq AF rules (increase when failed htlcs + no inbound). Default 15', 'min':0, 'max':100})
form.append({'unit': '%', 'form_id': 'af_excess', 'value': 95, 'label': 'AF Excess', 'id': 'AF-ExcessLimit', 'title': 'Limit for running excess liq AF rules (decrease for stagnant channels and those with assisting revenues). Default 95', 'min':0, 'max':100})
if 'GUI-' in prefixes:
form.append({'unit': '', 'form_id': 'gui_graphLinks', 'value': 'https://mempool.space/lightning', 'label': 'Graph URL', 'id': 'GUI-GraphLinks', 'title': 'Preferred Graph URL. Default https://mempool.space/lightning'})
form.append({'unit': '', 'form_id': 'gui_netLinks', 'value': 'https://mempool.space', 'label': 'NET URL', 'id': 'GUI-NetLinks', 'title': 'Preferred NET URL. Default https://mempool.space'})
if 'LND-' in prefixes:
form.append({'unit': '', 'form_id': 'lnd_cleanPayments', 'value': 0, 'label': 'LND Clean Payments', 'id': 'LND-CleanPayments', 'title': 'Clean LND Payments (toggles failed payment clean-up routine)', 'min':0, 'max':1})
form.append({'unit': 'days', 'form_id': 'lnd_retentionDays', 'value': 30, 'label': 'LND Retention', 'id': 'LND-RetentionDays', 'title': 'LND Retention days for failed payment data', 'min':1, 'max':1000})
form.append({'unit': 'min', 'form_id': 'lnd_reconnectInterval', 'value': 3, 'label': 'LND Reconnect Interval', 'id': 'LND-ReconnectInterval', 'title': 'Minimum minutes between reconnect attempts per inactive peer. Default 3', 'min':1, 'max':1440})
for prefix in prefixes:
ar_settings = LocalSettings.objects.filter(key__contains=prefix).values('key', 'value').order_by('key')
for field in form:
for sett in ar_settings:
if field['id'] == sett['key']:
field['value'] = sett['value']
break
return form
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def update_settings(request):
if request.method == 'POST':
template = [{'form_id': 'enabled', 'value': 0, 'parse': lambda x: int(x),'id': 'AR-Enabled'},
{'form_id': 'target_percent', 'value': 3.0, 'parse': lambda x: float(x),'id': 'AR-Target%'},
{'form_id': 'target_time', 'value': 5, 'parse': lambda x: int(x),'id': 'AR-Time'},
{'form_id': 'fee_rate', 'value': 500, 'parse': lambda x: int(x),'id': 'AR-MaxFeeRate'},
{'form_id': 'outbound_percent', 'value': 75, 'parse': lambda x: int(x),'id': 'AR-Outbound%'},
{'form_id': 'inbound_percent', 'value': 90, 'parse': lambda x: int(x),'id': 'AR-Inbound%'},
{'form_id': 'max_cost', 'value': 65, 'parse': lambda x: int(x),'id': 'AR-MaxCost%'},
{'form_id': 'variance', 'value': 0, 'parse': lambda x: int(x),'id': 'AR-Variance'},
{'form_id': 'wait_period', 'value': 30, 'parse': lambda x: int(x),'id': 'AR-WaitPeriod'},
{'form_id': 'autopilot', 'value': 0, 'parse': lambda x: int(x),'id': 'AR-Autopilot'},
{'form_id': 'autopilotdays', 'value': 7, 'parse': lambda x: int(x),'id': 'AR-APDays'},
{'form_id': 'workers', 'value': 1, 'parse': lambda x: int(x),'id': 'AR-Workers'},
#AF
{'form_id': 'af_enabled', 'value': 0, 'parse': lambda x: int(x),'id': 'AF-Enabled'},
{'form_id': 'af_inbound', 'value': 0, 'parse': lambda x: int(x),'id': 'AF-InboundFees'},
{'form_id': 'af_maxRate', 'value': 2500, 'parse': lambda x: int(x),'id': 'AF-MaxRate'},
{'form_id': 'af_minRate', 'value': 0, 'parse': lambda x: int(x),'id': 'AF-MinRate'},
{'form_id': 'af_increment', 'value': 5, 'parse': lambda x: int(x),'id': 'AF-Increment'},
{'form_id': 'af_multiplier', 'value': 5, 'parse': lambda x: int(x),'id': 'AF-Multiplier'},
{'form_id': 'af_failedHTLCs', 'value': 25, 'parse': lambda x: int(x),'id': 'AF-FailedHTLCs'},
{'form_id': 'af_updateHours', 'value': 24, 'parse': lambda x: int(x),'id': 'AF-UpdateHours'},
{'form_id': 'af_lowliq', 'value': 15, 'parse': lambda x: int(x),'id': 'AF-LowLiqLimit'},
{'form_id': 'af_excess', 'value': 95, 'parse': lambda x: int(x),'id': 'AF-ExcessLimit'},
#GUI
{'form_id': 'gui_graphLinks', 'value': 'https://mempool.space/lightning', 'parse': lambda x: str(x),'id': 'GUI-GraphLinks'},
{'form_id': 'gui_netLinks', 'value': 'https://mempool.space', 'parse': lambda x: str(x),'id': 'GUI-NetLinks'},
#LND
{'form_id': 'lnd_cleanPayments', 'value': 0, 'parse': lambda x: int(x), 'id': 'LND-CleanPayments'},
{'form_id': 'lnd_retentionDays', 'value': 30, 'parse': lambda x: int(x), 'id': 'LND-RetentionDays'},
{'form_id': 'lnd_reconnectInterval', 'value': 3, 'parse': lambda x: int(x), 'id': 'LND-ReconnectInterval'},
]
form = LocalSettingsForm(request.POST)
if not form.is_valid():
messages.error(request, 'Invalid Request. Please try again.')
else:
update_channels = form.cleaned_data['update_channels']
for field in template:
value = form.cleaned_data[field['form_id']]
if value is not None:
value = field['parse'](value)
try:
db_value = LocalSettings.objects.get(key=field['id'])
except:
LocalSettings(key=field['id'], value=field['value']).save()
db_value = LocalSettings.objects.get(key=field['id'])
if db_value.value == str(value) or len(str(value)) == 0:
continue
db_value.value = value
db_value.save()
if update_channels and field['id'] in ['AR-Target%', 'AR-Outbound%','AR-Inbound%','AR-MaxCost%']:
if field['id'] == 'AR-Target%':
Channels.objects.all().update(ar_amt_target=Round(F('capacity')*(value/100), output_field=IntegerField()))
elif field['id'] == 'AR-Outbound%':
Channels.objects.all().update(ar_out_target=value)
elif field['id'] == 'AR-Inbound%':
Channels.objects.all().update(ar_in_target=value)
elif field['id'] == 'AR-MaxCost%':
Channels.objects.all().update(ar_max_cost=value)
messages.success(request, 'All channels ' + field['id'] + ' updated to: ' + str(value))
else:
messages.success(request, field['id'] + ' updated to: ' + str(value))
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def update_channel(request):
if request.method == 'POST':
form = UpdateChannel(request.POST)
if form.is_valid() and Channels.objects.filter(chan_id=form.cleaned_data['chan_id']).exists():
chan_id = form.cleaned_data['chan_id']
target = form.cleaned_data['target']
update_target = int(form.cleaned_data['update_target'])
db_channel = Channels.objects.get(chan_id=chan_id)
if update_target == 0:
stub = lnrpc.LightningStub(lnd_connect())
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=target, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv))
db_channel.local_base_fee = target
db_channel.save()
messages.success(request, 'Base fee for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target))
elif update_target == 1:
stub = lnrpc.LightningStub(lnd_connect())
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(target/1000000), time_lock_delta=db_channel.local_cltv))
old_fee_rate = db_channel.local_fee_rate
db_channel.local_fee_rate = target
db_channel.fees_updated = datetime.now()
db_channel.save()
Autofees(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, setting=(f"Manual"), old_value=old_fee_rate, new_value=db_channel.local_fee_rate).save()
messages.success(request, 'Fee rate for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target))
elif update_target == 12:
stub = lnrpc.LightningStub(lnd_connect())
version = stub.GetInfo(ln.GetInfoRequest()).version
if float(version[:4]) >= 0.18:
channel_point = point(db_channel)
inbound_fee_rate = db_channel.local_inbound_fee_rate if db_channel.local_inbound_fee_rate else 0
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, inbound_fee=ln.InboundFee(base_fee_msat=target, fee_rate_ppm=inbound_fee_rate)))
db_channel.local_inbound_base_fee = target
db_channel.save()
messages.success(request, 'Inbound base fee for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target))
else:
messages.error(request, f'LND version too low to set inbound fees, update to v0.18+')
elif update_target == 13:
stub = lnrpc.LightningStub(lnd_connect())
version = stub.GetInfo(ln.GetInfoRequest()).version
if float(version[:4]) >= 0.18:
channel_point = point(db_channel)
inbound_base_fee = db_channel.local_inbound_base_fee if db_channel.local_inbound_base_fee else 0
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, inbound_fee=ln.InboundFee(base_fee_msat=inbound_base_fee, fee_rate_ppm=target)))
db_channel.local_inbound_fee_rate = target
db_channel.save()
messages.success(request, 'Inbound fee rate for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target))
else:
messages.error(request, f'LND version too low to set inbound fees, update to v0.18+')
elif update_target == 2:
db_channel.ar_amt_target = target
db_channel.save()
messages.success(request, 'Auto rebalancer target amount for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target))
elif update_target == 3:
db_channel.ar_in_target = target
db_channel.save()
messages.success(request, 'Auto rebalancer inbound target for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target) + '%')
elif update_target == 4:
db_channel.ar_out_target = target
db_channel.save()
messages.success(request, 'Auto rebalancer outbound target for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target) + '%')
elif update_target == 5:
db_channel.auto_rebalance = True if db_channel.auto_rebalance == False else False
db_channel.save()
messages.success(request, 'Auto rebalancer status for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(db_channel.auto_rebalance))
elif update_target == 6:
db_channel.ar_max_cost = target
db_channel.save()
messages.success(request, 'Auto rebalancer max cost for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target) + '%')
elif update_target == 7:
stub = lnrouter.RouterStub(lnd_connect())
channel_point = point(db_channel)
stub.UpdateChanStatus(lnr.UpdateChanStatusRequest(chan_point=channel_point, action=0)) if target == 1 else stub.UpdateChanStatus(lnr.UpdateChanStatusRequest(chan_point=channel_point, action=1))
db_channel.local_disabled = False if target == 1 else True
db_channel.save()
messages.success(request, 'Toggled channel state for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') to a value of: ' + ('Enabled' if target == 1 else 'Disabled'))
if target == 0:
messages.warning(request, 'Use with caution, while a channel is disabled (local fees highlighted in red) it will not route out.')
elif update_target == 8:
db_channel.auto_fees = True if db_channel.auto_fees == False else False
db_channel.save()
messages.success(request, 'Auto fees status for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(db_channel.auto_fees))
elif update_target == 9:
stub = lnrpc.LightningStub(lnd_connect())
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=target))
db_channel.local_cltv = target
db_channel.save()
messages.success(request, 'CLTV for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(float(target)))
elif update_target == 10:
stub = lnrpc.LightningStub(lnd_connect())
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, min_htlc_msat_specified=True, min_htlc_msat=int(target*1000)))
db_channel.local_min_htlc_msat = int(target*1000)
db_channel.save()
messages.success(request, 'Min HTLC for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(float(target)))
elif update_target == 11:
stub = lnrpc.LightningStub(lnd_connect())
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, max_htlc_msat=int(target*1000)))
db_channel.local_max_htlc_msat = int(target*1000)
db_channel.save()
messages.success(request, 'Max HTLC for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target))
else:
messages.error(request, 'Invalid target code. Please try again.')
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def update_pending(request):
if request.method == 'POST':
form = UpdatePending(request.POST)
if form.is_valid():
funding_txid = form.cleaned_data['funding_txid']
output_index = form.cleaned_data['output_index']
target = form.cleaned_data['target']
update_target = int(form.cleaned_data['update_target'])
if PendingChannels.objects.filter(funding_txid=funding_txid, output_index=output_index).exists():
pending_channel = PendingChannels.objects.filter(funding_txid=funding_txid, output_index=output_index)[0]
else:
pending_channel = PendingChannels(funding_txid=funding_txid, output_index=output_index)
pending_channel.save()
if update_target == 0:
pending_channel.local_base_fee = target
pending_channel.save()
messages.success(request, 'Base fee for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target))
elif update_target == 1:
pending_channel.local_fee_rate = target
pending_channel.save()
messages.success(request, 'Fee rate for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target))
elif update_target == 2:
pending_channel.ar_amt_target = target
pending_channel.save()
messages.success(request, 'Auto rebalancer target amount for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target))
elif update_target == 3:
pending_channel.ar_in_target = target
pending_channel.save()
messages.success(request, 'Auto rebalancer inbound target for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target) + '%')
elif update_target == 4:
pending_channel.ar_out_target = target
pending_channel.save()
messages.success(request, 'Auto rebalancer outbound target for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target) + '%')
elif update_target == 5:
pending_channel.auto_rebalance = True if pending_channel.auto_rebalance == False or pending_channel.auto_rebalance == None else False
pending_channel.save()
messages.success(request, 'Auto rebalancer status for pending pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(pending_channel.auto_rebalance))
elif update_target == 6:
pending_channel.ar_max_cost = target
pending_channel.save()
messages.success(request, 'Auto rebalancer max cost for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target) + '%')
elif update_target == 8:
auto_fees = int(LocalSettings.objects.filter(key='AF-Enabled')[0].value) if LocalSettings.objects.filter(key='AF-Enabled').exists() else 0
pending_channel.auto_fees = True if pending_channel.auto_fees == False or (pending_channel.auto_fees == None and auto_fees == 0) else False
pending_channel.save()
messages.success(request, 'Auto fees status for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(pending_channel.auto_fees))
elif update_target == 9:
pending_channel.local_cltv = target
pending_channel.save()
messages.success(request, 'CLTV for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target))
else:
messages.error(request, 'Invalid target code. Please try again.')
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
def point(ch: Channels):
channel_point = ln.ChannelPoint()
channel_point.funding_txid_bytes = bytes.fromhex(ch.funding_txid)
channel_point.funding_txid_str = ch.funding_txid
channel_point.output_index = ch.output_index
return channel_point
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def update_setting(request):
if request.method == 'POST':
form = UpdateSetting(request.POST)
if form.is_valid():
key = form.cleaned_data['key']
value = form.cleaned_data['value']
if key == 'ALL-oRate':
target = int(value)
stub = lnrpc.LightningStub(lnd_connect())
channels = Channels.objects.filter(is_open=True)
for db_channel in channels:
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(target/1000000), time_lock_delta=db_channel.local_cltv))
old_fee_rate = db_channel.local_fee_rate
db_channel.local_fee_rate = target
db_channel.fees_updated = datetime.now()
db_channel.save()
Autofees(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, setting=(f"Manual"), old_value=old_fee_rate, new_value=db_channel.local_fee_rate).save()
messages.success(request, 'Fee rate for all open channels updated to a value of: ' + str(target))
elif key == 'ALL-oBase':
target = int(value)
stub = lnrpc.LightningStub(lnd_connect())
channels = Channels.objects.filter(is_open=True)
for db_channel in channels:
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=target, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv))
db_channel.local_base_fee = target
db_channel.save()
messages.success(request, 'Base fee for all channels updated to a value of: ' + str(target))
elif key == 'ALL-iRate':
target = int(value)
stub = lnrpc.LightningStub(lnd_connect())
version = stub.GetInfo(ln.GetInfoRequest()).version
if float(version[:4]) >= 0.18:
channels = Channels.objects.filter(is_open=True)
for db_channel in channels:
channel_point = point(db_channel)
inbound_base_fee = db_channel.local_inbound_base_fee if db_channel.local_inbound_base_fee else 0
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, inbound_fee=ln.InboundFee(base_fee_msat=inbound_base_fee, fee_rate_ppm=target)))
db_channel.local_inbound_fee_rate = target
db_channel.save()
messages.success(request, 'Inbound fee rate for all open channels updated to a value of: ' + str(target))
else:
messages.error(request, f'LND version too low to set inbound fees, update to v0.18+')
elif key == 'ALL-iBase':
target = int(value)
stub = lnrpc.LightningStub(lnd_connect())
version = stub.GetInfo(ln.GetInfoRequest()).version
if float(version[:4]) >= 0.18:
channels = Channels.objects.filter(is_open=True)
for db_channel in channels:
channel_point = point(db_channel)
inbound_fee_rate = db_channel.local_inbound_fee_rate if db_channel.local_inbound_fee_rate else 0
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, inbound_fee=ln.InboundFee(base_fee_msat=target, fee_rate_ppm=inbound_fee_rate)))
db_channel.local_inbound_base_fee = target
db_channel.save()
messages.success(request, 'Inbound base fee for all channels updated to a value of: ' + str(target))
else:
messages.error(request, f'LND version too low to set inbound fees, update to v0.18+')
elif key == 'ALL-CLTV':
target = int(value)
stub = lnrpc.LightningStub(lnd_connect())
channels = Channels.objects.filter(is_open=True)
for db_channel in channels:
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=target))
db_channel.local_cltv = target
db_channel.save()
messages.success(request, 'CLTV for all channels updated to a value of: ' + str(target))
elif key == 'ALL-minHTLC':
target = int(float(value)*1000)
stub = lnrpc.LightningStub(lnd_connect())
channels = Channels.objects.filter(is_open=True)
for db_channel in channels:
channel_point = point(db_channel)
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, min_htlc_msat_specified=True, min_htlc_msat=target))
db_channel.local_min_htlc_msat = target
db_channel.save()
messages.success(request, 'Min HTLC for all channels updated to a value of: ' + str(float(value)))
elif key == 'ALL-Amts':
target = int(value)
channels = Channels.objects.filter(is_open=True).update(ar_amt_target=target)
messages.success(request, 'AR target amounts for all channels updated to a value of: ' + str(target))
elif key == 'ALL-MaxCost':
target = int(value)
channels = Channels.objects.filter(is_open=True).update(ar_max_cost=target)
messages.success(request, 'AR max cost %s for all channels updated to a value of: ' + str(target))
elif key == 'ALL-oTarget':
target = int(value)
channels = Channels.objects.filter(is_open=True).update(ar_out_target=target)
messages.success(request, 'AR outbound liquidity target %s for all channels updated to a value of: ' + str(target))
elif key == 'ALL-iTarget':
target = int(value)
channels = Channels.objects.filter(is_open=True).update(ar_in_target=target)
messages.success(request, 'AR inbound liquidity target %s for all channels updated to a value of: ' + str(target))
elif key == 'ALL-AR':
target = int(value)
channels = Channels.objects.filter(is_open=True).update(auto_rebalance=target)
messages.success(request, 'Auto-Rebalance targeting for all channels updated to a value of: ' + str(target))
elif key == 'ALL-AF':
target = int(value)
channels = Channels.objects.filter(is_open=True, private=False).update(auto_fees=target)
messages.success(request, 'Auto Fees setting for all channels updated to a value of: ' + str(target))
try:
db_enabled = LocalSettings.objects.get(key='AF-UpdateHours')
except:
LocalSettings(key='AF-UpdateHours', value='24').save()
db_enabled = LocalSettings.objects.get(key='AF-UpdateHours')
db_enabled.value = target
db_enabled.save()
messages.success(request, 'Updated autofees update hours setting to: ' + str(target))
else:
messages.error(request, 'Invalid Request. Please try again. [' + key +']')
else:
messages.error(request, 'Invalid Request Form. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def update_closing(request):
if request.method == 'POST':
form = UpdateClosing(request.POST)
if form.is_valid() and Closures.objects.filter(funding_txid=form.cleaned_data['funding_txid'], funding_index=form.cleaned_data['funding_index']).exists():
funding_txid = form.cleaned_data['funding_txid']
funding_index = form.cleaned_data['funding_index']
target = int(form.cleaned_data['target'])
db_closing = Closures.objects.filter(funding_txid=funding_txid, funding_index=funding_index)[0]
db_closing.closing_costs = target
db_closing.save()
messages.success(request, 'Updated closing costs for ' + str(funding_txid) + ':' + str(funding_index) + ' updated to a value of: ' + str(target))
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def update_keysend(request):
if request.method == 'POST':
form = UpdateKeysend(request.POST)
if form.is_valid() and Invoices.objects.filter(r_hash=form.cleaned_data['r_hash']).exists():
r_hash = form.cleaned_data['r_hash']
db_invoice = Invoices.objects.filter(r_hash=r_hash)[0]
db_invoice.is_revenue = not db_invoice.is_revenue
db_invoice.save()
messages.success(request, ('Marked' if db_invoice.is_revenue else 'Unmarked') + ' invoice ' + str(r_hash) + ' as revenue.')
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def add_avoid(request):
if request.method == 'POST':
form = AddAvoid(request.POST)
if form.is_valid():
pubkey = form.cleaned_data['pubkey']
notes = form.cleaned_data['notes']
AvoidNodes(pubkey=pubkey, notes=notes).save()
messages.success(request, 'Successfully added node ' + str(pubkey) + ' to the avoid list.')
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def remove_avoid(request):
if request.method == 'POST':
form = RemoveAvoid(request.POST)
if form.is_valid() and AvoidNodes.objects.filter(pubkey=form.cleaned_data['pubkey']).exists():
pubkey = form.cleaned_data['pubkey']
AvoidNodes.objects.filter(pubkey=pubkey).delete()
messages.success(request, 'Successfully removed node ' + str(pubkey) + ' from the avoid list.')
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect(request.META.get('HTTP_REFERER'))
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def get_fees(request):
if request.method == 'GET':
missing_fees = Closures.objects.exclude(close_type__in=[4, 5]).exclude(open_initiator=2, resolution_count=0).filter(closing_costs=0)
if missing_fees:
for missing_fee in missing_fees:
prior_closures = Closures.objects.filter(id__lt=missing_fee.id).values_list('chan_id', flat=True)
swept = list(Resolutions.objects.filter(chan_id__in=prior_closures).values_list('sweep_txid', flat=True))
try:
txid = missing_fee.closing_tx
closing_costs = get_tx_fees(txid) if missing_fee.open_initiator == 1 else 0
for resolution in Resolutions.objects.filter(chan_id=missing_fee.chan_id).exclude(resolution_type=2):
if resolution.sweep_txid not in swept:
closing_costs += get_tx_fees(resolution.sweep_txid)
swept.append(resolution.sweep_txid)
missing_fee.closing_costs = closing_costs
missing_fee.save()
except Exception as error:
messages.error(request, f"Error getting closure fees: {txid=} {error=}")
return redirect(request.META.get('HTTP_REFERER'))
return redirect(request.META.get('HTTP_REFERER'))
@api_view(['POST'])
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def sign_message(request):
serializer = SignMessageSerializer(data=request.data)
if serializer.is_valid():
message = serializer.validated_data['message']
try:
stub = lnrpc.LightningStub(lnd_connect())
response = stub.SignMessage(ln.SignMessageRequest(msg=message.encode('utf-8'), single_hash=False))
return Response({'message': 'Success', 'data': str(response.signature)})
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]
return Response({'error': f'Sign message failed! Error: {error_msg}'})
else:
return Response({'error': 'Invalid request!'})
class PaymentsViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Payments.objects.all().order_by('-creation_date')
serializer_class = PaymentSerializer
filterset_fields = {'status':['exact','lt','gt'], 'creation_date':['lt','lte','gte'], 'chan_out': ['exact'], 'index': ['lt']}
class PaymentHopsViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = PaymentHops.objects.all()
serializer_class = PaymentHopsSerializer
class InvoicesViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Invoices.objects.all().order_by('-creation_date')
serializer_class = InvoiceSerializer
filterset_fields = {'state': ['exact','lt', 'gt'], 'is_revenue': ['exact'], 'settle_date': ['gte'], 'chan_in': ['exact'], 'index': ['lt']}
def update(self, request, pk=None):
setting = get_object_or_404(Invoices.objects.all(), pk=pk)
serializer = InvoiceSerializer(setting, data=request.data, context={'request': request})
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
class ForwardsFilter(FilterSet):
chan_in_or_out = CharFilter(method='filter_chan_in_or_out', label='Chan In Or Out')
forward_date__lte = DateTimeFilter(field_name='forward_date', lookup_expr='lte')
forward_date__gte = DateTimeFilter(field_name='forward_date', lookup_expr='gte')
forward_date__lt = DateTimeFilter(field_name='forward_date', lookup_expr='lt')
forward_date__gt = DateTimeFilter(field_name='forward_date', lookup_expr='gt')
id__lt = NumberFilter(field_name='id', lookup_expr='lt')
def filter_chan_in_or_out(self, queryset, name, value):
return queryset.filter(
Q(chan_id_in__exact=value) | Q(chan_id_out__exact=value)
)
class Meta:
model = Forwards
fields = ['chan_in_or_out', 'forward_date__lte', 'forward_date__gte', 'forward_date__lt', 'forward_date__gt', 'id__lt']
class ForwardsViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Forwards.objects.all().order_by('-id')
serializer_class = ForwardSerializer
filterset_class = ForwardsFilter
class PeersViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Peers.objects.all()
serializer_class = PeerSerializer
class OnchainViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Onchain.objects.all()
serializer_class = OnchainSerializer
filterset_fields = {'time_stamp': ['lte','gte']}
class ClosuresViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Closures.objects.all()
serializer_class = ClosuresSerializer
filterset_fields = {'close_height': ['lte','gte']}
class ResolutionsViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Resolutions.objects.all()
serializer_class = ResolutionsSerializer
class PendingHTLCViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = PendingHTLCs.objects.all()
serializer_class = PendingHTLCSerializer
class PeerEventsViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = PeerEvents.objects.all().order_by('-id')
serializer_class = PeerEventsSerializer
filterset_fields = {'chan_id': ['exact'], 'id': ['lt']}
class TradeSalesViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = TradeSales.objects.all()
serializer_class = TradeSalesSerializer
def update(self, request, pk):
rebalance = get_object_or_404(self.queryset, pk=pk)
serializer = self.get_serializer(rebalance, data=request.data, context={'request': request}, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors)
class FeeLogViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Autofees.objects.all().order_by('-id')
serializer_class = FeeLogSerializer
filterset_fields = {'chan_id': ['exact'], 'id': ['lt']}
class InboundFeeLogViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = InboundFeeLog.objects.all().order_by('-id')
serializer_class = InboundFeeLogSerializer
filterset_fields = {'chan_id': ['exact'], 'id': ['lt']}
class FailedHTLCFilter(FilterSet):
chan_in_or_out = CharFilter(method='filter_chan_in_or_out', label='Chan In Or Out')
chan_id_in = CharFilter(field_name='chan_id_in', lookup_expr='exact')
chan_id_out = CharFilter(field_name='chan_id_out', lookup_expr='exact')
wire_failure__lt = NumberFilter(field_name='wire_failure', lookup_expr='lt')
wire_failure__gt = NumberFilter(field_name='wire_failure', lookup_expr='gt')
id__lt = NumberFilter(field_name='id', lookup_expr='lt')
def filter_chan_in_or_out(self, queryset, name, value):
return queryset.filter(
Q(chan_id_in__exact=value) | Q(chan_id_out__exact=value)
)
class Meta:
model = FailedHTLCs
fields = ['chan_in_or_out', 'chan_id_in', 'chan_id_out', 'wire_failure__lt', 'wire_failure__gt', 'id__lt']
class FailedHTLCViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = FailedHTLCs.objects.all().order_by('-id')
serializer_class = FailedHTLCSerializer
filterset_class = FailedHTLCFilter
class HistFailedHTLCViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = HistFailedHTLC.objects.all().order_by('-id')
serializer_class = HistFailedHTLCSerializer
class LocalSettingsViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = LocalSettings.objects.all()
serializer_class = LocalSettingsSerializer
def update(self, request, pk):
setting = get_object_or_404(self.queryset, pk=pk)
serializer = LocalSettingsSerializer(setting, data=request.data, context={'request': request})
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
class ChannelsViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Channels.objects.all()
serializer_class = ChannelSerializer
filterset_fields = ['is_open', 'private', 'is_active', 'auto_rebalance']
def update(self, request, pk):
channel = get_object_or_404(self.queryset, pk=pk)
serializer = ChannelSerializer(channel, data=request.data, context={'request': request}, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors)
class RebalancerViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else []
queryset = Rebalancer.objects.all().order_by('-id')
serializer_class = RebalancerSerializer
filterset_fields = {'status':['lt','gt','exact'], 'payment_hash':['exact'], 'stop':['gt'], 'last_hop_pubkey':['exact'], 'id':['lt']}
def create(self, request):
serializer = self.get_serializer(data=request.data, context={'request': request})
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors)
def update(self, request, pk):
rebalance = get_object_or_404(self.queryset, pk=pk)
serializer = RebalancerSerializer(rebalance, data=request.data, context={'request': request}, partial=True)
if serializer.is_valid():
rebalance.stop = datetime.now()
serializer.save()
return Response(serializer.data)
return Response(serializer.errors)
@api_view(['GET'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def forwards_summary(request):
filter_1day = datetime.now() - timedelta(days=1)
filter_7day = datetime.now() - timedelta(days=7)
summary_out = Forwards.objects.values(chan_id=F('chan_id_out')).annotate(
count_outgoing_1day=Count('id', filter=Q(forward_date__gte=filter_1day)),
sum_outgoing_1day=Coalesce(Sum('amt_out_msat', filter=Q(forward_date__gte=filter_1day)), 0),
count_outgoing_7day=Count('id', filter=Q(forward_date__gte=filter_7day)),
sum_outgoing_7day=Coalesce(Sum('amt_out_msat', filter=Q(forward_date__gte=filter_7day)), 0),
sum_fees_1day=Coalesce(Sum('fee', filter=Q(forward_date__gte=filter_1day)), 0.0),
sum_fees_7day=Coalesce(Sum('fee', filter=Q(forward_date__gte=filter_7day)), 0.0),
count_incoming_1day=Value(0),
sum_incoming_1day=Value(0),
count_incoming_7day=Value(0),
sum_incoming_7day=Value(0)
).filter(
Q(count_outgoing_1day__gt=0) |
Q(sum_outgoing_1day__gt=0) |
Q(count_outgoing_7day__gt=0) |
Q(sum_outgoing_7day__gt=0) |
Q(sum_fees_1day__gt=0) |
Q(sum_fees_7day__gt=0)
)
summary_in = Forwards.objects.values(chan_id=F('chan_id_in')).annotate(
count_outgoing_1day=Value(0),
sum_outgoing_1day=Value(0),
count_outgoing_7day=Value(0),
sum_outgoing_7day=Value(0),
sum_fees_1day=Value(0),
sum_fees_7day=Value(0),
count_incoming_1day=Count('id', filter=Q(forward_date__gte=filter_1day)),
sum_incoming_1day=Coalesce(Sum('amt_in_msat', filter=Q(forward_date__gte=filter_1day)), 0),
count_incoming_7day=Count('id', filter=Q(forward_date__gte=filter_7day)),
sum_incoming_7day=Coalesce(Sum('amt_in_msat', filter=Q(forward_date__gte=filter_7day)), 0)
).filter(
Q(count_incoming_1day__gt=0) |
Q(sum_incoming_1day__gt=0) |
Q(count_incoming_7day__gt=0) |
Q(sum_incoming_7day__gt=0)
)
return Response({'results': summary_out.union(summary_in)})
def get_channeldb_file_size():
try:
# Create the Enable setting if it doesn't exist ---
enabled_setting = LocalSettings.objects.filter(key='RemoteFSEnabled').first()
if not enabled_setting:
LocalSettings.objects.create(key='RemoteFSEnabled', value='0') # Default: disabled
# IMPORTANT: We do NOT create other settings here.
# Only once enabled at /api/settings/RemoteFSEnabled/, we create the rest to avoid boilerplate
enabled = False # Since we just created it, it's disabled.
else:
# Read the Enable setting ---
enabled = bool(int(LocalSettings.objects.get(key='RemoteFSEnabled').value))
if enabled:
# Only import paramiko if enabled
import paramiko
# Create connection settings only if enabled AND they don't exist ---
host = LocalSettings.objects.filter(key='RemoteFSHost').first()
if not host:
LocalSettings.objects.create(key='RemoteFSHost', value='')
host_value = LocalSettings.objects.get(key='RemoteFSHost').value
else:
host_value = host.value
user = LocalSettings.objects.filter(key='RemoteFSUser').first()
if not user:
LocalSettings.objects.create(key='RemoteFSUser', value='')
user_value = LocalSettings.objects.get(key='RemoteFSUser').value
else:
user_value = user.value
port = LocalSettings.objects.filter(key='RemoteFSPort').first()
if not port:
LocalSettings.objects.create(key='RemoteFSPort', value='22') # Default SSH port
port_value = LocalSettings.objects.get(key='RemoteFSPort').value
else:
port_value = port.value
db_path = LocalSettings.objects.filter(key='RemoteFSPath').first()
if not db_path:
default_path = '/home/admin/.lnd/data/graph/mainnet/channel.db'
LocalSettings.objects.create(key='RemoteFSPath', value=default_path)
db_path_value = LocalSettings.objects.get(key='RemoteFSPath').value
else:
db_path_value = db_path.value
# Read the connection settings ---
port = int(port_value) # Ensure port is an integer
channel_db_path = db_path_value if db_path_value else settings.LND_DATABASE_PATH # Use settings path if db path is blank
# Check for required settings
if not host_value or not user_value:
logger.error('Error: Remote file size enabled, but host or user is not set')
return round(path.getsize(path.expanduser(settings.LND_DATABASE_PATH))*0.000000001, 3)
# --- Paramiko logic ---
try:
# Create SSH client
ssh = paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
# Connect to the remote host (eg 10.1.1.2, lnd, 22)
ssh.connect(hostname=host_value, username=user_value, port=port)
# Open an SFTP session
sftp = ssh.open_sftp()
# Get file stats
file_stat = sftp.stat(channel_db_path)
# Get file size
file_size_bytes = file_stat.st_size
# Close connections
sftp.close()
ssh.close()
return round(file_size_bytes * 0.000000001, 3)
except Exception as e:
logger.error(f'Error retrieving file size with paramiko: {str(e)}')
return round(path.getsize(path.expanduser(settings.LND_DATABASE_PATH))*0.000000001, 3) # Fallback
# --- End Paramiko logic ---
else:
# Use the original default behavior (local file size)
return round(path.getsize(path.expanduser(settings.LND_DATABASE_PATH))*0.000000001, 3)
except Exception as e:
# Handle exceptions
logger.error(f'Error retrieving channel.db file size: {str(e)}')
return 0
@api_view(['GET'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def node_info(request):
stub = lnrpc.LightningStub(lnd_connect())
node_info = stub.GetInfo(ln.GetInfoRequest())
balances = stub.WalletBalance(ln.WalletBalanceRequest())
pending_channels = stub.PendingChannels(ln.PendingChannelsRequest())
limbo_balance = pending_channels.total_limbo_balance
pending_open = None
pending_closed = None
pending_force_closed = None
waiting_for_close = None
pending_open_balance = 0
pending_closing_balance = 0
if pending_channels.pending_open_channels:
target_resp = pending_channels.pending_open_channels
peers = Peers.objects.all()
pending_changes = PendingChannels.objects.all()
pending_open = []
inbound_setting = int(LocalSettings.objects.filter(key='AR-Inbound%')[0].value) if LocalSettings.objects.filter(key='AR-Inbound%').exists() else 90
outbound_setting = int(LocalSettings.objects.filter(key='AR-Outbound%')[0].value) if LocalSettings.objects.filter(key='AR-Outbound%').exists() else 75
amt_setting = float(LocalSettings.objects.filter(key='AR-Target%')[0].value) if LocalSettings.objects.filter(key='AR-Target%').exists() else 3
cost_setting = int(LocalSettings.objects.filter(key='AR-MaxCost%')[0].value) if LocalSettings.objects.filter(key='AR-MaxCost%').exists() else 65
auto_fees = int(LocalSettings.objects.filter(key='AF-Enabled')[0].value) if LocalSettings.objects.filter(key='AF-Enabled').exists() else 0
for i in range(0,len(target_resp)):
item = {}
pending_open_balance += target_resp[i].channel.local_balance
funding_txid = target_resp[i].channel.channel_point.split(':')[0]
output_index = target_resp[i].channel.channel_point.split(':')[1]
updated = pending_changes.filter(funding_txid=funding_txid,output_index=output_index).exists()
item['alias'] = peers.filter(pubkey=target_resp[i].channel.remote_node_pub)[0].alias if peers.filter(pubkey=target_resp[i].channel.remote_node_pub).exists() else ''
item['remote_node_pub'] = target_resp[i].channel.remote_node_pub
item['channel_point'] = target_resp[i].channel.channel_point
item['funding_txid'] = funding_txid
item['output_index'] = output_index
item['capacity'] = target_resp[i].channel.capacity
item['local_balance'] = target_resp[i].channel.local_balance
item['remote_balance'] = target_resp[i].channel.remote_balance
item['local_chan_reserve_sat'] = target_resp[i].channel.local_chan_reserve_sat
item['remote_chan_reserve_sat'] = target_resp[i].channel.remote_chan_reserve_sat
item['initiator'] = target_resp[i].channel.initiator
item['commitment_type'] = target_resp[i].channel.commitment_type
item['commit_fee'] = target_resp[i].commit_fee
item['commit_weight'] = target_resp[i].commit_weight
item['fee_per_kw'] = target_resp[i].fee_per_kw
item['local_base_fee'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].local_base_fee if updated else ''
item['local_fee_rate'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].local_fee_rate if updated else ''
item['local_cltv'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].local_cltv if updated else ''
item['auto_rebalance'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].auto_rebalance if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].auto_rebalance != None else False
item['ar_amt_target'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_amt_target if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_amt_target != None else int((amt_setting/100) * target_resp[i].channel.capacity)
item['ar_in_target'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_in_target if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_in_target != None else inbound_setting
item['ar_out_target'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_out_target if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_out_target != None else outbound_setting
item['ar_max_cost'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_max_cost if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_max_cost != None else cost_setting
item['auto_fees'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].auto_fees if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].auto_fees != None else (False if auto_fees == 0 else True)
pending_open.append(item)
if pending_channels.pending_closing_channels:
target_resp = pending_channels.pending_closing_channels
pending_closed = []
for i in range(0,len(target_resp)):
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,
'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type, 'local_commit_fee_sat': target_resp[i].commitments.local_commit_fee_sat,'limbo_balance':target_resp[i].limbo_balance,'closing_txid':target_resp[i].closing_txid}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
pending_closed.append(pending_item)
if pending_channels.pending_force_closing_channels:
target_resp = pending_channels.pending_force_closing_channels
pending_force_closed = []
for i in range(0,len(target_resp)):
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'initiator':target_resp[i].channel.initiator,
'commitment_type':target_resp[i].channel.commitment_type,'closing_txid':target_resp[i].closing_txid,'limbo_balance':target_resp[i].limbo_balance,'maturity_height':target_resp[i].maturity_height,'blocks_til_maturity':target_resp[i].blocks_til_maturity if target_resp[i].blocks_til_maturity > 0 else find_next_block_maturity(target_resp[i]),
'maturity_datetime':(datetime.now()+timedelta(minutes=(10*target_resp[i].blocks_til_maturity if target_resp[i].blocks_til_maturity > 0 else 10*find_next_block_maturity(target_resp[i]) )))}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
pending_force_closed.append(pending_item)
if pending_channels.waiting_close_channels:
target_resp = pending_channels.waiting_close_channels
waiting_for_close = []
for i in range(0,len(target_resp)):
pending_closing_balance += target_resp[i].limbo_balance
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,
'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type, 'local_commit_fee_sat': target_resp[i].commitments.local_commit_fee_sat, 'limbo_balance':target_resp[i].limbo_balance,'closing_txid':target_resp[i].closing_txid}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
waiting_for_close.append(pending_item)
limbo_balance -= pending_closing_balance
try:
db_size = get_channeldb_file_size()
except:
db_size = 0
return Response({
'version': node_info.version,
'num_peers': node_info.num_peers,
'synced_to_graph': node_info.synced_to_graph,
'synced_to_chain': node_info.synced_to_chain,
'num_active_channels': node_info.num_active_channels,
'num_inactive_channels': node_info.num_inactive_channels,
'chains': [chain.chain+"-"+chain.network for chain in node_info.chains],
'block': {'hash': node_info.block_hash, 'height': node_info.block_height},
'balance': {
'limbo': limbo_balance,
'onchain': balances.total_balance,
'confirmed': balances.confirmed_balance,
'unconfirmed': balances.unconfirmed_balance,
'total': balances.total_balance + pending_open_balance + limbo_balance,
},
'pending_open': pending_open,
'pending_closed': pending_closed,
'pending_force_closed': pending_force_closed,
'waiting_for_close': waiting_for_close,
'db_size': db_size
})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def connect_peer(request):
serializer = ConnectPeerSerializer(data=request.data)
if serializer.is_valid():
try:
stub = lnrpc.LightningStub(lnd_connect())
peer_id = serializer.validated_data['peer_id']
if peer_id.count('@') == 0 and len(peer_id) == 66:
peer_pubkey = peer_id
node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node
host = node.addresses[0].addr
elif peer_id.count('@') == 1 and len(peer_id.split('@')[0]) == 66:
peer_pubkey, host = peer_id.split('@')
else:
return Response({'error': 'Invalid peer pubkey or connection string.'})
ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host)
stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr))
return Response({'message': 'Connection successful!'})
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]
return Response({'error': 'Connection request failed! Error: ' + error_msg})
else:
return Response({'error': 'Invalid request!'})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def disconnect_peer(request):
serializer = DisconnectPeerSerializer(data=request.data)
if serializer.is_valid():
try:
stub = lnrpc.LightningStub(lnd_connect())
peer_id = serializer.validated_data['peer_id']
if len(peer_id) == 66:
peer_pubkey = peer_id
else:
return Response({'error': 'Invalid peer pubkey.'})
stub.DisconnectPeer(ln.DisconnectPeerRequest(pub_key=peer_pubkey))
if Peers.objects.filter(pubkey=peer_id).exists():
db_peer = Peers.objects.filter(pubkey=peer_id)[0]
db_peer.connected = False
db_peer.save()
return Response({'message': 'Disconnection successful!'})
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]
return Response({'error': 'Connection request failed! Error: ' + error_msg})
else:
return Response({'error': 'Invalid request!'})
@api_view(['GET'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def rebalance_stats(request):
try:
filter_7day = datetime.now() - timedelta(days=7)
rebalances = Rebalancer.objects.filter(stop__gt=filter_7day).values('last_hop_pubkey').annotate(attempts=Count('last_hop_pubkey'), successes=Sum(Case(When(status=2, then=1), output_field=IntegerField())))
return Response(rebalances)
except Exception as e:
error = str(e)
return Response({'error': 'Unable to fetch stats! Error: ' + error})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def open_channel(request):
serializer = OpenChannelSerializer(data=request.data)
if serializer.is_valid():
try:
stub = lnrpc.LightningStub(lnd_connect())
peer_pubkey = serializer.validated_data['peer_pubkey']
connected = False
if Peers.objects.filter(pubkey=peer_pubkey, connected=True).exists():
connected = True
else:
try:
node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node
host = node.addresses[0].addr
ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host)
response = stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr))
connected = True
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]
return Response({'error': 'Error connecting to new peer: ' + error_msg})
if connected:
for response in stub.OpenChannel(ln.OpenChannelRequest(node_pubkey=bytes.fromhex(peer_pubkey), local_funding_amount=serializer.validated_data['local_amt'], sat_per_byte=serializer.validated_data['sat_per_byte'])):
return Response({'message': 'Channel created! Funding TXID: ' + str(response.chan_pending.txid[::-1].hex()) + ':' + str(response.chan_pending.output_index)})
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]
return Response({'error': 'Channel creation failed! Error: ' + error_msg})
else:
return Response({'error': 'Invalid request!'})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def close_channel(request):
serializer = CloseChannelSerializer(data=request.data)
if serializer.is_valid():
try:
chan_id = serializer.validated_data['chan_id']
if chan_id.count('x') == 2 and len(chan_id) >= 5:
target_channel = Channels.objects.filter(short_chan_id=chan_id)
else:
target_channel = Channels.objects.filter(chan_id=chan_id)
if target_channel.exists():
target_channel = target_channel.get()
funding_txid = target_channel.funding_txid
output_index = target_channel.output_index
target_fee = serializer.validated_data['target_fee']
channel_point = ln.ChannelPoint()
channel_point.funding_txid_bytes = bytes.fromhex(funding_txid)
channel_point.funding_txid_str = funding_txid
channel_point.output_index = output_index
stub = lnrpc.LightningStub(lnd_connect())
if serializer.validated_data['force']:
for response in stub.CloseChannel(ln.CloseChannelRequest(channel_point=channel_point, force=True)):
return Response({'message': 'Channel force closed! Closing TXID: ' + str(response.close_pending.txid[::-1].hex()) + ':' + str(response.close_pending.output_index)})
else:
for response in stub.CloseChannel(ln.CloseChannelRequest(channel_point=channel_point, sat_per_byte=target_fee)):
return Response({'message': 'Channel gracefully closed! Closing TXID: ' + str(response.close_pending.txid[::-1].hex()) + ':' + str(response.close_pending.output_index)})
else:
return Response({'error': 'Channel ID is not valid.'})
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]
return Response({'error': 'Channel close failed! Error: ' + error_msg})
else:
return Response({'error': 'Invalid request!'})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def add_invoice(request):
serializer = AddInvoiceSerializer(data=request.data)
if serializer.is_valid() and serializer.validated_data['value'] >= 0:
try:
stub = lnrpc.LightningStub(lnd_connect())
response = stub.AddInvoice(ln.Invoice(value=serializer.validated_data['value']))
return Response({'message': 'Invoice created!', 'data':str(response.payment_request)})
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]
return Response({'error': 'Invoice creation failed! Error: ' + error_msg})
else:
return Response({'error': 'Invalid request!'})
def get_new_address(stub, legacy=False):
version = stub.GetInfo(ln.GetInfoRequest()).version
# Verify sufficient version to handle p2tr address creation
if float(version[:4]) >= 0.15 and not legacy:
response = stub.NewAddress(ln.NewAddressRequest(type=4))
else:
response = stub.NewAddress(ln.NewAddressRequest(type=0))
return response
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def new_address(request):
serializer = NewAddressSerializer(data=request.data)
if serializer.is_valid():
try:
stub = lnrpc.LightningStub(lnd_connect())
response = get_new_address(stub, legacy=serializer.validated_data['legacy'])
return Response({'message': 'Retrieved new deposit address!', 'data':str(response.address)})
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]
return Response({'error': 'Address creation failed! Error: ' + error_msg})
else:
return Response({'error': 'Invalid request!'}, status=400)
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def consolidate_utxos(request):
serializer = ConsolidateSerializer(data=request.data)
if serializer.is_valid():
try:
stub = lnrpc.LightningStub(lnd_connect())
self_addr = get_new_address(stub).address
txid = stub.SendCoins(ln.SendCoinsRequest(addr=self_addr, send_all=True, sat_per_vbyte=serializer.validated_data['sat_per_vbyte'])).txid
return Response({'message': f'Successfully consolidated UXTOs: {txid}', 'txid':txid})
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]
return Response({'error': 'Failed to consolidate utxos! Error: ' + error_msg})
else:
return Response({'error': 'Invalid request!'}, status=400)
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def update_alias(request):
serializer = UpdateAliasSerializer(data=request.data)
if serializer.is_valid():
peer_pubkey = serializer.validated_data['peer_pubkey']
if Channels.objects.filter(remote_pubkey=peer_pubkey).exists():
try:
stub = lnrpc.LightningStub(lnd_connect())
new_alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey)).node.alias
update_channels = Channels.objects.filter(remote_pubkey=peer_pubkey)
for channel in update_channels:
channel.alias = new_alias
channel.save()
messages.success(request, 'Alias updated to: ' + str(new_alias))
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]
messages.error(request, 'Error updating alias: ' + error_msg)
else:
messages.error(request, 'Pubkey not in channels list.')
else:
messages.error(request, 'Invalid Request. Please try again.')
return redirect('home')
@api_view(['GET'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def get_info(request):
try:
stub = lnrpc.LightningStub(lnd_connect())
response = stub.GetInfo(ln.GetInfoRequest())
target = {'identity_pubkey':response.identity_pubkey, 'alias':response.alias, 'num_active_channels':response.num_active_channels, 'num_peers':response.num_peers, 'block_height':response.block_height, 'block_hash':response.block_hash,'synced_to_chain':response.synced_to_chain,'testnet':response.testnet,'uris':[uri for uri in response.uris],'best_header_timestamp':response.best_header_timestamp,'version':response.version,'num_inactive_channels':response.num_inactive_channels,'chains':[{'chain':response.chains[i].chain,'network':response.chains[i].network} for i in range(0,len(response.chains))],'color':response.color,'synced_to_graph':response.synced_to_graph}
return Response({'message': 'success', 'data':target})
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]
return Response({'error': 'Failed to call getinfo! Error: ' + error_msg})
@api_view(['GET'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def api_balances(request):
try:
stub = lnrpc.LightningStub(lnd_connect())
balances = stub.WalletBalance(ln.WalletBalanceRequest())
pending_channels = stub.PendingChannels(ln.PendingChannelsRequest())
limbo_balance = pending_channels.total_limbo_balance
pending_open_balance = 0
if pending_channels.pending_open_channels:
target_resp = pending_channels.pending_open_channels
for i in range(0,len(target_resp)):
pending_open_balance += target_resp[i].channel.local_balance
channels = Channels.objects.filter(is_open=1)
offchain_balance = channels.aggregate(Sum('local_balance'))['local_balance__sum'] + channels.aggregate(Sum('pending_outbound'))['pending_outbound__sum'] + pending_open_balance + limbo_balance
target = {'total_balance':(balances.total_balance + offchain_balance),'offchain_balance':offchain_balance,'onchain_balance':balances.total_balance, 'confirmed_balance':balances.confirmed_balance, 'unconfirmed_balance':balances.unconfirmed_balance}
return Response({'message': 'success', 'data':target})
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]
return Response({'error': 'Failed to get wallet balances! Error: ' + error_msg})
@api_view(['GET'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def api_income(request):
try:
stub = lnrpc.LightningStub(lnd_connect())
try:
days = int(request.GET.urlencode()[1:])
except:
days = None
day_filter = datetime.now() - timedelta(days=days) if days else None
node_info = stub.GetInfo(ln.GetInfoRequest())
payments = Payments.objects.filter(status=2).filter(creation_date__gte=day_filter) if day_filter else Payments.objects.filter(status=2)
onchain_txs = Onchain.objects.filter(time_stamp__gte=day_filter) if day_filter else Onchain.objects.all()
closures = Closures.objects.filter(close_height__gte=(node_info.block_height - (days*144))) if days else Closures.objects.all()
forwards = Forwards.objects.filter(forward_date__gte=day_filter) if day_filter else Forwards.objects.all()
forward_count = forwards.count()
forward_amount = 0 if forward_count == 0 else int(forwards.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000)
total_revenue = 0 if forward_count == 0 else int(forwards.aggregate(Sum('fee'))['fee__sum'])
invoices = Invoices.objects.filter(state=1, is_revenue=True).filter(settle_date__gte=day_filter) if day_filter else Invoices.objects.filter(state=1, is_revenue=True)
total_received = 0 if invoices.count() == 0 else int(invoices.aggregate(Sum('amt_paid'))['amt_paid__sum'])
total_revenue += total_received
total_revenue_ppm = 0 if forward_amount == 0 else int(total_revenue/(forward_amount/1000000))
total_sent = 0 if payments.count() == 0 else int(payments.aggregate(Sum('value'))['value__sum'])
total_fees = 0 if payments.count() == 0 else int(payments.aggregate(Sum('fee'))['fee__sum'])
total_fees_ppm = 0 if total_sent == 0 else int(total_fees/(total_sent/1000000))
onchain_costs = 0 if onchain_txs.count() == 0 else onchain_txs.aggregate(Sum('fee'))['fee__sum']
close_fees = closures.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures.exists() else 0
onchain_costs += close_fees
profits = int(total_revenue-total_fees-onchain_costs)
target = {
'forward_count': forward_count,
'forward_amount': forward_amount,
'total_revenue': total_revenue,
'total_revenue_ppm': total_revenue_ppm,
'total_fees': total_fees,
'total_fees_ppm': total_fees_ppm,
'onchain_costs': onchain_costs,
'profits': profits,
'profits_ppm': 0 if forward_amount == 0 else int(profits/(forward_amount/1000000)),
'percent_cost': 0 if total_revenue == 0 else int(((total_fees+onchain_costs)/total_revenue)*100),
}
return Response({'message': 'success', 'data':target})
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]
return Response({'error': 'Failed to get revenue stats! Error: ' + error_msg})
@api_view(['GET'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def pending_channels(request):
try:
stub = lnrpc.LightningStub(lnd_connect())
response = stub.PendingChannels(ln.PendingChannelsRequest())
if response.pending_open_channels or response.pending_closing_channels or response.pending_force_closing_channels or response.waiting_close_channels or response.total_limbo_balance:
target = {}
if response.pending_open_channels:
target_resp = response.pending_open_channels
peers = Peers.objects.all()
pending_open_channels = []
for i in range(0,len(target_resp)):
pending_item = {'alias':peers.filter(pubkey=target_resp[i].channel.remote_node_pub)[0].alias if peers.filter(pubkey=target_resp[i].channel.remote_node_pub).exists() else None,
'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,
'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type,'commit_fee':target_resp[i].commit_fee,'commit_weight':target_resp[i].commit_weight,'fee_per_kw':target_resp[i].fee_per_kw}
pending_open_channels.append(pending_item)
target.update({'pending_open': pending_open_channels})
if response.pending_closing_channels:
target_resp = response.pending_closing_channels
pending_closing_channels = []
for i in range(0,len(target_resp)):
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,
'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type,'limbo_balance':target_resp[i].limbo_balance}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
pending_closing_channels.append(pending_item)
target.update({'pending_closing':pending_closing_channels})
if response.pending_force_closing_channels:
target_resp = response.pending_force_closing_channels
pending_force_closing_channels = []
for i in range(0,len(target_resp)):
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'initiator':target_resp[i].channel.initiator,
'commitment_type':target_resp[i].channel.commitment_type,'closing_txid':target_resp[i].closing_txid,'limbo_balance':target_resp[i].limbo_balance,'maturity_height':target_resp[i].maturity_height,'blocks_til_maturity':target_resp[i].blocks_til_maturity,'maturity_datetime':(datetime.now()+timedelta(minutes=(10*target_resp[i].blocks_til_maturity)))}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
pending_force_closing_channels.append(pending_item)
target.update({'pending_force_closing':pending_force_closing_channels})
if response.waiting_close_channels:
target_resp = response.waiting_close_channels
waiting_close_channels = []
for i in range(0,len(target_resp)):
pending_item = {'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,
'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type,'limbo_balance':target_resp[i].limbo_balance}
pending_item.update(pending_channel_details(target_resp[i].channel.channel_point))
waiting_close_channels.append(pending_item)
target.update({'waiting_close':waiting_close_channels})
if response.total_limbo_balance:
total_limbo_balance = {'total_limbo_balance':response.total_limbo_balance}
target.update(total_limbo_balance)
return Response({'message': 'success', 'data':target})
else:
return Response({'message': 'success', 'data':None})
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]
return Response({'error': 'Failed to get pending channels! Error: ' + error_msg})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def bump_fee(request):
serializer = BumpFeeSerializer(data=request.data)
if serializer.is_valid():
txid = serializer.validated_data['txid']
index = serializer.validated_data['index']
target_fee = serializer.validated_data['target_fee']
force = serializer.validated_data['force']
try:
target_outpoint = ln.OutPoint()
target_outpoint.txid_str = txid
target_outpoint.output_index = index
stub = walletstub.WalletKitStub(lnd_connect())
stub.BumpFee(walletrpc.BumpFeeRequest(outpoint=target_outpoint, sat_per_vbyte=target_fee, force=force))
return Response({'message': f'Fee bumped to {target_fee} sats/vbyte for outpoint: {txid}:{index}'})
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]
return Response({'error': f'Fee bump failed! Error: {error_msg}'})
else:
return Response({'error': 'Invalid request!'})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def chan_policy(request):
serializer = UpdateChanPolicy(data=request.data)
if serializer.is_valid() and Channels.objects.filter(chan_id=serializer.validated_data['chan_id']).exists():
chan_id = serializer.validated_data['chan_id']
db_channel = Channels.objects.get(chan_id=chan_id)
channel_point = point(db_channel)
return_response = {}
try:
if serializer.validated_data['base_fee'] is not None or serializer.validated_data['fee_rate'] is not None or serializer.validated_data['cltv'] is not None or serializer.validated_data['min_htlc'] is not None or serializer.validated_data['max_htlc'] is not None or serializer.validated_data['inbound_base_fee'] is not None or serializer.validated_data['inbound_fee_rate'] is not None:
base_fee_msat = serializer.validated_data['base_fee'] if serializer.validated_data['base_fee'] is not None else db_channel.local_base_fee
fee_rate = (serializer.validated_data['fee_rate']/1000000) if serializer.validated_data['fee_rate'] is not None else (db_channel.local_fee_rate/1000000)
inbound_base_fee_msat = serializer.validated_data['inbound_base_fee'] if serializer.validated_data['inbound_base_fee'] is not None else db_channel.local_inbound_base_fee
inbound_fee_rate = serializer.validated_data['inbound_fee_rate'] if serializer.validated_data['inbound_fee_rate'] is not None else db_channel.local_inbound_fee_rate
time_lock_delta = serializer.validated_data['cltv'] if serializer.validated_data['cltv'] is not None else db_channel.local_cltv
min_htlc_msat = int(serializer.validated_data['min_htlc']*1000) if serializer.validated_data['min_htlc'] is not None else db_channel.local_min_htlc_msat
max_htlc_msat = int(serializer.validated_data['max_htlc']*1000) if serializer.validated_data['max_htlc'] is not None else db_channel.local_max_htlc_msat
stub = lnrpc.LightningStub(lnd_connect())
version = stub.GetInfo(ln.GetInfoRequest()).version
kwargs = {'chan_point':channel_point, 'base_fee_msat':base_fee_msat, 'fee_rate':fee_rate, 'time_lock_delta':time_lock_delta, 'min_htlc_msat_specified':True, 'min_htlc_msat':min_htlc_msat, 'max_htlc_msat':max_htlc_msat}
if serializer.validated_data['inbound_base_fee'] is not None or serializer.validated_data['inbound_fee_rate'] is not None:
if float(version[:4]) >= 0.18:
kwargs['inbound_fee'] = ln.InboundFee(base_fee_msat = inbound_base_fee_msat if inbound_base_fee_msat else 0, fee_rate_ppm = inbound_fee_rate if inbound_fee_rate else 0)
else:
return Response({'error': f'LND version too low to set inbound fees, update to v0.18+'})
stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(**kwargs))
if serializer.validated_data['base_fee'] is not None:
db_channel.local_base_fee = serializer.validated_data['base_fee']
db_channel.save()
return_response['base_fee'] = serializer.validated_data['base_fee']
if serializer.validated_data['fee_rate'] is not None:
old_fee_rate = db_channel.local_fee_rate
db_channel.local_fee_rate = serializer.validated_data['fee_rate']
db_channel.fees_updated = datetime.now()
db_channel.save()
return_response['fee_rate'] = serializer.validated_data['fee_rate']
Autofees(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, setting=(f"Manual"), old_value=old_fee_rate, new_value=db_channel.local_fee_rate).save()
if serializer.validated_data['inbound_base_fee'] is not None:
db_channel.local_inbound_base_fee = serializer.validated_data['inbound_base_fee']
db_channel.save()
return_response['inbound_base_fee'] = serializer.validated_data['inbound_base_fee']
if serializer.validated_data['inbound_fee_rate'] is not None:
db_channel.local_inbound_fee_rate = serializer.validated_data['inbound_fee_rate']
db_channel.save()
return_response['inbound_fee_rate'] = serializer.validated_data['inbound_fee_rate']
if serializer.validated_data['cltv'] is not None:
db_channel.local_cltv = serializer.validated_data['cltv']
db_channel.save()
return_response['cltv'] = serializer.validated_data['cltv']
if serializer.validated_data['min_htlc'] is not None:
db_channel.local_min_htlc_msat = int(serializer.validated_data['min_htlc']*1000)
db_channel.save()
return_response['min_htlc'] = serializer.validated_data['min_htlc']
if serializer.validated_data['max_htlc'] is not None:
db_channel.local_max_htlc_msat = int(serializer.validated_data['max_htlc']*1000)
db_channel.save()
return_response['max_htlc'] = serializer.validated_data['max_htlc']
if serializer.validated_data['disabled'] is not None:
stub = lnrouter.RouterStub(lnd_connect())
stub.UpdateChanStatus(lnr.UpdateChanStatusRequest(chan_point=channel_point, action=0)) if serializer.validated_data['disabled'] == 0 else stub.UpdateChanStatus(lnr.UpdateChanStatusRequest(chan_point=channel_point, action=1))
db_channel.local_disabled = False if serializer.validated_data['disabled'] == 0 else True
db_channel.save()
return_response['disabled'] = serializer.validated_data['base_fee']
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]
return Response({'error': f'Channel policy update failed! Error: {error_msg}'})
return Response(return_response)
else:
return Response({'error': 'Invalid request!'})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def broadcast_tx(request):
serializer = BroadcastTXSerializer(data=request.data)
if serializer.is_valid():
raw_tx = serializer.validated_data['raw_tx']
try:
stub = walletstub.WalletKitStub(lnd_connect())
response = stub.PublishTransaction(walletrpc.Transaction(tx_hex=bytes.fromhex(raw_tx)))
if response.publish_error == '':
return Response({'message': f'Successfully broadcast tx!'})
else:
return Response({'error': f'Error while broadcasting TX: {response.publish_error}'})
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]
return Response({'error': f'TX broadcast failed! Error: {error_msg}'})
else:
return Response({'error': 'Invalid request!'})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def create_trade(request):
serializer = CreateTradeSerializer(data=request.data)
if serializer.is_valid():
description = serializer.validated_data['description']
price = serializer.validated_data['price']
sale_type = serializer.validated_data['type']
secret = serializer.validated_data['secret']
expiry = serializer.validated_data['expiry']
sale_limit = serializer.validated_data['sale_limit']
trade_id = token_bytes(32).hex()
try:
new_trade = TradeSales(id=trade_id, description=description, price=price, secret=secret, expiry=expiry, sale_type=sale_type, sale_limit=sale_limit)
new_trade.save()
return Response({'message': f'Created trade: {description}', 'id': new_trade.id, 'description': new_trade.description, 'price': new_trade.price, 'expiry': new_trade.expiry, 'sale_type': new_trade.sale_type, 'secret': new_trade.secret, 'sale_count': new_trade.sale_count, 'sale_limit': new_trade.sale_limit})
except Exception as e:
error = str(e)
return Response({'error': f'Error creating trade: {error}'})
else:
return Response({'error': serializer.error_messages})
@api_view(['POST'])
@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED)
def reset_api(request):
serializer = ResetSerializer(data=request.data)
if serializer.is_valid():
table = serializer.validated_data['table']
tables = {
'Forwards': Forwards.objects.all(),
'Payments': Payments.objects.all(),
'PaymentHops': PaymentHops.objects.all(),
'Invoices': Invoices.objects.all(),
'Rebalancer': Rebalancer.objects.all(),
'Closures': Closures.objects.all(),
'Resolutions': Resolutions.objects.all(),
'Peers': Peers.objects.all(),
'Channels': Channels.objects.all(),
'PendingChannels': PendingChannels.objects.all(),
'Onchain': Onchain.objects.all(),
'PendingHTLCs': PendingHTLCs.objects.all(),
'FailedHTLCs': FailedHTLCs.objects.all(),
'HistFailedHTLC': HistFailedHTLC.objects.all(),
'Autopilot': Autopilot.objects.all(),
'Autofees': Autofees.objects.all(),
'AvoidNodes': AvoidNodes.objects.all(),
'PeerEvents': PeerEvents.objects.all(),
'LocalSettings': LocalSettings.objects.all()
}
try:
target_table = tables[table]
target_table.delete()
return Response({'message': f'Successfully deleted table: {table}'})
except Exception as e:
error = str(e)
return Response({'error': f'Error deleting table: {error}'})
else:
return Response({'error': serializer.error_messages})
@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED)
def peer_offline_report(request):
# Timeframe is now fixed to Month to Date
# timeframe_options = {'MTD': 'Month to Date'} # No longer needed for a dropdown
# selected_timeframe = 'MTD' # Implied
alias_filter_query = request.GET.get('alias', '').strip()
sort_by_query = request.GET.get('sort', '-total_offline_hours')
now = timezone.now()
start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
peer_stats_list = []
open_channels = Channels.objects.filter(is_open=True)
if alias_filter_query:
open_channels = open_channels.filter(alias__icontains=alias_filter_query)
for channel in open_channels:
events = PeerEvents.objects.filter(
chan_id=channel.chan_id,
event='Connection',
timestamp__gte=start_date
).order_by('timestamp')
if not events.exists() and not (alias_filter_query and channel.alias.lower().__contains__(alias_filter_query.lower())):
# Skip if no events and not specifically filtered for this alias (to show 0s)
continue
total_offline_duration_seconds = 0
offline_instance_count = 0
current_offline_start_time = None
for i, event in enumerate(events):
if event.new_value == 0:
if current_offline_start_time is None:
current_offline_start_time = event.timestamp
offline_instance_count += 1
elif event.new_value == 1:
if current_offline_start_time is not None:
duration = event.timestamp - current_offline_start_time
total_offline_duration_seconds += duration.total_seconds()
current_offline_start_time = None
if current_offline_start_time is not None:
if not channel.is_active:
duration = now - current_offline_start_time
total_offline_duration_seconds += duration.total_seconds()
current_peer_stats = {
'chan_id': channel.chan_id,
'channel_alias': channel.alias,
'avg_offline_hours': 0,
'total_offline_hours': 0,
'offline_count': 0,
'currently_offline': not channel.is_active,
}
if offline_instance_count > 0:
avg_offline_hours = (total_offline_duration_seconds / offline_instance_count / 3600.0)
sum_offline_hours = total_offline_duration_seconds / 3600.0
current_peer_stats['avg_offline_hours'] = round(avg_offline_hours, 2)
current_peer_stats['total_offline_hours'] = round(sum_offline_hours, 2)
current_peer_stats['offline_count'] = offline_instance_count
# Add to list if it had offline events OR if it was specifically filtered by alias (to show 0s)
if offline_instance_count > 0 or (alias_filter_query and channel.alias.lower().__contains__(alias_filter_query.lower())):
peer_stats_list.append(current_peer_stats)
# Python-side sorting for initial load / if JS is disabled / if alias filter is used
reverse_sort = sort_by_query.startswith('-')
# Use the part after '-' as the key, ensure it matches dict keys
sort_key = sort_by_query.lstrip('-')
def sort_helper(item):
val = item.get(sort_key)
if isinstance(val, bool):
return (val is False, val) if reverse_sort else (val is True, val)
if isinstance(val, (int, float)):
return val
if val is None:
return float('-inf') if not reverse_sort else float('inf')
return str(val).lower()
peer_stats_list.sort(key=sort_helper, reverse=reverse_sort)
context = {
'peer_stats': peer_stats_list,
'alias_filter_query': alias_filter_query,
'page_title': 'Peer Offline Report (Month to Date)', # Updated title
'active_menu': 'peer_offline_report',
}
return render(request, 'peer_offline_report.html', context)