@@ -386,7 +387,7 @@
Last 5 Payments Received
State |
Channel In Alias |
Channel In |
-
Keysend |
+
Keysend |
{% for invoice in invoices %}
diff --git a/gui/templates/keysends.html b/gui/templates/keysends.html
new file mode 100644
index 00000000..4f1c0881
--- /dev/null
+++ b/gui/templates/keysends.html
@@ -0,0 +1,30 @@
+{% extends "base.html" %}
+{% block content %}
+{% load humanize %}
+{% if keysends %}
+
+
Received Keysends
+
+
+ Settle Date |
+ Channel In Alias |
+ Amount |
+ Message |
+
+ {% for keysend in keysends %}
+
+ {{ keysend.settle_date|naturaltime }} |
+ {{ keysend.chan_in_alias }} |
+ {{ keysend.amt_paid|intcomma }} |
+ {{ keysend.message }} |
+
+ {% endfor %}
+
+
+{% endif %}
+{% if not keysends %}
+
+
You dont have any keysend messages here yet!
+
+{% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/gui/templates/open_list.html b/gui/templates/open_list.html
index 3ee936c7..af80cb27 100644
--- a/gui/templates/open_list.html
+++ b/gui/templates/open_list.html
@@ -13,6 +13,7 @@ Suggested Open List
Fees Paid |
Effective PPM |
Volume Score |
+ Savings By Volume |
{% for node in open_list %}
@@ -23,6 +24,7 @@ Suggested Open List
{{ node.fees|add:"0"|intcomma }} |
{{ node.ppm|add:"0"|intcomma }} |
{{ node.score }} |
+ {{ node.sum_cost_to|add:"0"|intcomma }} |
{% endfor %}
diff --git a/gui/templates/route.html b/gui/templates/route.html
index 6fbacb51..2affa7fb 100644
--- a/gui/templates/route.html
+++ b/gui/templates/route.html
@@ -11,6 +11,7 @@
Route For Payment: {{ payment_hash }}
Alias |
Channel ID |
Channel Capacity |
+
Cost To |
{% for hop in route %}
@@ -20,6 +21,7 @@ Route For Payment: {{ payment_hash }}
{{ hop.alias }} |
{{ hop.chan_id }} |
{{ hop.chan_capacity|intcomma }} |
+ {{ hop.cost_to }} |
{% endfor %}
diff --git a/gui/urls.py b/gui/urls.py
index 1535b082..549726b8 100644
--- a/gui/urls.py
+++ b/gui/urls.py
@@ -33,6 +33,8 @@
path('ar_target/', views.ar_target, name='ar-target'),
path('suggested_opens/', views.suggested_opens, name='suggested-opens'),
path('suggested_actions/', views.suggested_actions, name='suggested-actions'),
+ path('keysends/', views.keysends, name='keysends'),
+ path('autopilot/', views.autopilot, name='autopilot'),
path('api/', include(router.urls), name='api-root'),
path('api-auth/', include('rest_framework.urls'), name='api-auth'),
path('api/connectpeer/', views.connect_peer, name='connect-peer'),
@@ -42,4 +44,4 @@
path('api/newaddress/', views.new_address, name='new-address'),
path('api/updatealias/', views.update_alias, name='update-alias'),
path('lndg-admin/', admin.site.urls),
-]
+]
\ No newline at end of file
diff --git a/gui/views.py b/gui/views.py
index d9c85b42..87079839 100644
--- a/gui/views.py
+++ b/gui/views.py
@@ -9,12 +9,13 @@
from rest_framework.response import Response
from rest_framework.decorators import api_view
from .forms import OpenChannelForm, CloseChannelForm, ConnectPeerForm, AddInvoiceForm, RebalancerForm, ChanPolicyForm, AutoRebalanceForm, ARTarget
-from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, PendingHTLCs, FailedHTLCs
+from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, PendingHTLCs, FailedHTLCs, Autopilot
from .serializers import ConnectPeerSerializer, FailedHTLCSerializer, LocalSettingsSerializer, OpenChannelSerializer, CloseChannelSerializer, AddInvoiceSerializer, PaymentHopsSerializer, PaymentSerializer, InvoiceSerializer, ForwardSerializer, ChannelSerializer, PendingHTLCSerializer, RebalancerSerializer, UpdateAliasSerializer, PeerSerializer, OnchainSerializer, PendingHTLCs, FailedHTLCs
from .lnd_deps import lightning_pb2 as ln
from .lnd_deps import lightning_pb2_grpc as lnrpc
from .lnd_deps.lnd_connect import lnd_connect
-from lndg.settings import LND_NETWORK
+from lndg.settings import LND_NETWORK, LND_DIR_PATH
+from os import path
@login_required(login_url='/lndg-admin/login/?next=/')
def home(request):
@@ -101,11 +102,16 @@ def home(request):
total_costs_7day = total_7day_fees + onchain_costs_7day
#Get list of recent rebalance requests
rebalances = Rebalancer.objects.all().order_by('-requested')
- #Grab local settings
+ total_channels = node_info.num_active_channels + node_info.num_inactive_channels
local_settings = LocalSettings.objects.all()
+ try:
+ db_size = round(path.getsize(path.expanduser(LND_DIR_PATH + '/data/graph/' + LND_NETWORK + '/channel.db'))*0.000000001, 3)
+ except:
+ db_size = 0
#Build context for front-end and render page
context = {
'node_info': node_info,
+ 'total_channels': total_channels,
'balances': balances,
'payments': payments[:6],
'total_sent': int(total_sent),
@@ -151,7 +157,8 @@ def home(request):
'7day_routed_ppm': 0 if routed_7day_amt == 0 else int((total_earned_7day/routed_7day_amt)*1000000),
'7day_payments_ppm': 0 if payments_7day_amt == 0 else int((total_7day_fees/payments_7day_amt)*1000000),
'liq_ratio': 0 if total_outbound == 0 else int((total_inbound/sum_outbound)*100),
- 'network': 'testnet/' if LND_NETWORK == 'testnet' else ''
+ 'network': 'testnet/' if LND_NETWORK == 'testnet' else '',
+ 'db_size': db_size
}
return render(request, 'home.html', context)
else:
@@ -199,7 +206,7 @@ def suggested_opens(request):
current_peers = Channels.objects.filter(is_open=True).values_list('remote_pubkey')
filter_60day = datetime.now() - timedelta(days=60)
payments_60day = Payments.objects.filter(creation_date__gte=filter_60day).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).values('node_pubkey', 'alias').annotate(ppm=(Sum('fee')/Sum('amt'))*1000000).annotate(score=Round((Round(Count('id')/5, output_field=IntegerField())+Round(Sum('amt')/500000, output_field=IntegerField()))/10, output_field=IntegerField())).annotate(count=Count('id')).annotate(amount=Sum('amt')).annotate(fees=Sum('fee')).order_by('-score', 'ppm')[:21]
+ open_list = PaymentHops.objects.filter(payment_hash__in=payments_60day).exclude(node_pubkey=self_pubkey).exclude(node_pubkey__in=current_peers).values('node_pubkey', 'alias').annotate(ppm=(Sum('fee')/Sum('amt'))*1000000).annotate(score=Round((Round(Count('id')/5, output_field=IntegerField())+Round(Sum('amt')/500000, output_field=IntegerField()))/10, output_field=IntegerField())).annotate(count=Count('id')).annotate(amount=Sum('amt')).annotate(fees=Sum('fee')).annotate(sum_cost_to=Sum('cost_to')/(Sum('amt')/1000000)).order_by('-score', 'ppm')[:21]
context = {
'open_list': open_list
}
@@ -238,6 +245,11 @@ def suggested_actions(request):
print('Case 1: Pass')
continue
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:
+ print('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
print('Case 2: Enable AR')
result['output'] = 'Enable AR'
result['reason'] = 'o7D > i7D AND Inbound Liq > 75%'
@@ -271,6 +283,26 @@ def pending_htlcs(request):
else:
return redirect('home')
+@login_required(login_url='/lndg-admin/login/?next=/')
+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')
+
+@login_required(login_url='/lndg-admin/login/?next=/')
+def autopilot(request):
+ if request.method == 'GET':
+ context = {
+ 'autopilot': Autopilot.objects.all().order_by('-timestamp')
+ }
+ return render(request, 'autopilot.html', context)
+ else:
+ return redirect('home')
+
@login_required(login_url='/lndg-admin/login/?next=/')
def open_channel_form(request):
if request.method == 'POST':
diff --git a/jobs.py b/jobs.py
index 09f73f87..6f0d1503 100644
--- a/jobs.py
+++ b/jobs.py
@@ -65,11 +65,14 @@ def update_payments(stub):
if attempt.status == 1:
hops = attempt.route.hops
hop_count = 0
+ cost_to = 0
total_hops = len(hops)
for hop in hops:
hop_count += 1
alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=hop.pub_key, include_channels=False)).node.alias
- PaymentHops(payment_hash=db_payment, attempt_id=attempt.attempt_id, step=hop_count, chan_id=hop.chan_id, alias=alias, chan_capacity=hop.chan_capacity, node_pubkey=hop.pub_key, amt=round(hop.amt_to_forward_msat/1000, 3), fee=round(hop.fee_msat/1000, 3)).save()
+ fee = hop.fee_msat/1000
+ PaymentHops(payment_hash=db_payment, attempt_id=attempt.attempt_id, step=hop_count, chan_id=hop.chan_id, alias=alias, chan_capacity=hop.chan_capacity, node_pubkey=hop.pub_key, amt=round(hop.amt_to_forward_msat/1000, 3), fee=round(fee, 3), cost_to=round(cost_to, 3)).save()
+ cost_to += fee
if hop_count == 1:
db_payment.chan_out = hop.chan_id
db_payment.chan_out_alias = alias
@@ -126,23 +129,30 @@ def update_channels(stub):
db_channel.alias = alias
db_channel.funding_txid = txid
db_channel.output_index = index
- chan_data = stub.GetChanInfo(ln.ChanInfoRequest(chan_id=channel.chan_id))
- if chan_data.node1_pub == channel.remote_pubkey:
- local_policy = chan_data.node2_policy
- remote_policy = chan_data.node1_policy
- else:
- local_policy = chan_data.node1_policy
- remote_policy = chan_data.node2_policy
+ try:
+ chan_data = stub.GetChanInfo(ln.ChanInfoRequest(chan_id=channel.chan_id))
+ if chan_data.node1_pub == channel.remote_pubkey:
+ db_channel.local_base_fee = chan_data.node2_policy.fee_base_msat
+ db_channel.local_fee_rate = chan_data.node2_policy.fee_rate_milli_msat
+ db_channel.remote_base_fee = chan_data.node1_policy.fee_base_msat
+ db_channel.remote_fee_rate = chan_data.node1_policy.fee_rate_milli_msat
+ else:
+ db_channel.local_base_fee = chan_data.node1_policy.fee_base_msat
+ db_channel.local_fee_rate = chan_data.node1_policy.fee_rate_milli_msat
+ db_channel.remote_base_fee = chan_data.node2_policy.fee_base_msat
+ db_channel.remote_fee_rate = chan_data.node2_policy.fee_rate_milli_msat
+ except:
+ db_channel.local_base_fee = 0
+ db_channel.local_fee_rate = 0
+ db_channel.remote_base_fee = 0
+ db_channel.remote_fee_rate = 0
db_channel.capacity = channel.capacity
db_channel.local_balance = channel.local_balance
db_channel.remote_balance = channel.remote_balance
db_channel.unsettled_balance = channel.unsettled_balance
db_channel.local_commit = channel.commit_fee
db_channel.local_chan_reserve = channel.local_chan_reserve_sat
- db_channel.local_base_fee = local_policy.fee_base_msat
- db_channel.local_fee_rate = local_policy.fee_rate_milli_msat
- db_channel.remote_base_fee = remote_policy.fee_base_msat
- db_channel.remote_fee_rate = remote_policy.fee_rate_milli_msat
+ db_channel.num_updates = channel.num_updates
db_channel.is_active = channel.active
db_channel.is_open = True
db_channel.save()
diff --git a/rebalancer.py b/rebalancer.py
index 702db154..7ffcca63 100644
--- a/rebalancer.py
+++ b/rebalancer.py
@@ -2,7 +2,7 @@
from django.conf import settings
from django.db.models import Sum
from pathlib import Path
-from datetime import datetime
+from datetime import datetime, timedelta
from gui.lnd_deps import lightning_pb2 as ln
from gui.lnd_deps import lightning_pb2_grpc as lnrpc
from gui.lnd_deps import router_pb2 as lnr
@@ -20,7 +20,7 @@
)
django.setup()
from lndg import settings
-from gui.models import Rebalancer, Channels, LocalSettings
+from gui.models import Rebalancer, Channels, LocalSettings, Forwards, Autopilot
def run_rebalancer(rebalance):
if Rebalancer.objects.filter(status=1).exists():
@@ -112,7 +112,7 @@ def auto_schedule():
# TLDR: lets target a custom % of the amount that would bring us back to a 50/50 channel balance using the MaxFeerate to calculate sat fee intervals
for target in inbound_cans:
target_fee_rate = int(target.local_fee_rate * max_cost)
- if target_fee_rate > 0:
+ if target_fee_rate > 0 and target_fee_rate > target.remote_fee_rate:
value_per_fee = int(1 / (target_fee_rate / 1000000)) if target_fee_rate <= max_fee_rate else int(1 / (max_fee_rate / 1000000))
target_value = int((target.capacity * target_percent) / value_per_fee) * value_per_fee
if target_value >= value_per_fee:
@@ -137,9 +137,44 @@ def auto_schedule():
print('Target Time:', target_time)
Rebalancer(value=target_value, fee_limit=target_fee, outgoing_chan_ids=outbound_cans, last_hop_pubkey=inbound_pubkey.remote_pubkey, target_alias=inbound_pubkey.alias, duration=target_time).save()
+def auto_enable():
+ if LocalSettings.objects.filter(key='AR-Autopilot').exists():
+ enabled = int(LocalSettings.objects.filter(key='AR-Autopilot')[0].value)
+ else:
+ LocalSettings(key='AR-Autopilot', value='0').save()
+ enabled = 0
+ if enabled == 1:
+ channels = Channels.objects.filter(is_active=True, is_open=True).annotate(outbound_percent=(Sum('local_balance')*1000)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*1000)/Sum('capacity'))
+ filter_7day = datetime.now() - timedelta(days=7)
+ forwards = Forwards.objects.filter(forward_date__gte=filter_7day)
+ for channel in channels:
+ outbound_percent = int(round(channel.outbound_percent/10, 0))
+ inbound_percent = int(round(channel.inbound_percent/10, 0))
+ routed_in_7day = forwards.filter(chan_id_in=channel.chan_id).count()
+ routed_out_7day = forwards.filter(chan_id_out=channel.chan_id).count()
+ i7D = 0 if 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
+ o7D = 0 if 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
+ if o7D > (i7D*1.10) and outbound_percent > 75:
+ print('Case 1: Pass')
+ elif o7D > (i7D*1.10) and inbound_percent > 75 and channel.auto_rebalance == False:
+ print('Case 2: Enable AR - o7D > i7D AND Inbound Liq > 75%')
+ channel.auto_rebalance = True
+ channel.save()
+ Autopilot(chan_id=channel.chan_id, peer_alias=channel.alias, setting='Enabled', old_value=0, new_value=1).save()
+ elif o7D < (i7D*1.10) and outbound_percent > 75 and channel.auto_rebalance == True:
+ print('Case 3: Disable AR - o7D < i7D AND Outbound Liq > 75%')
+ channel.auto_rebalance = False
+ channel.save()
+ Autopilot(chan_id=channel.chan_id, peer_alias=channel.alias, setting='Enabled', old_value=1, new_value=0).save()
+ elif o7D < (i7D*1.10) and inbound_percent > 75:
+ print('Case 4: Pass')
+ else:
+ print('Case 5: Pass')
+
def main():
rebalances = Rebalancer.objects.filter(status=0).order_by('id')
if len(rebalances) == 0:
+ auto_enable()
auto_schedule()
else:
run_rebalancer(rebalances[0])