From 03fe7b4261e1ce388162b9471bffaf633b8aae61 Mon Sep 17 00:00:00 2001 From: cryptosharks131 <38626122+cryptosharks131@users.noreply.github.com> Date: Sun, 9 Jan 2022 15:37:42 -0500 Subject: [PATCH] V1.0.1 (#17) * Suggested Peers Includes Savings By Volume * Rebalance attempts will no longer be submitted when (your fee * max cost) < peer fee * Fixes an edge case in the channels data when uptime of a channel is 0 and fee rates never get set * View keysends page * Autopilot mode --- README.md | 27 +++++++++------ gui/migrations/0016_auto_20220106_1026.py | 42 +++++++++++++++++++++++ gui/migrations/0017_autopilot.py | 26 ++++++++++++++ gui/models.py | 12 +++++++ gui/templates/autopilot.html | 35 +++++++++++++++++++ gui/templates/base.html | 2 +- gui/templates/home.html | 7 ++-- gui/templates/keysends.html | 30 ++++++++++++++++ gui/templates/open_list.html | 2 ++ gui/templates/route.html | 2 ++ gui/urls.py | 4 ++- gui/views.py | 42 ++++++++++++++++++++--- jobs.py | 34 +++++++++++------- rebalancer.py | 41 ++++++++++++++++++++-- 14 files changed, 270 insertions(+), 36 deletions(-) create mode 100644 gui/migrations/0016_auto_20220106_1026.py create mode 100644 gui/migrations/0017_autopilot.py create mode 100644 gui/templates/autopilot.html create mode 100644 gui/templates/keysends.html diff --git a/README.md b/README.md index 2ed47273..e51801be 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ docker-compose up -d 6. Initialize some settings for your django site (see notes below) `.venv/bin/python initialize.py` 7. The initial login user is `lndg-admin` and the password is output here: `lndg-admin.txt` 8. Generate some initial data for your dashboard `.venv/bin/python jobs.py` -9. Run the server via a python development server `.venv/bin/python manage.py runserver 0.0.0.0:8889` +9. Run the server via a python development server `.venv/bin/python manage.py runserver 0.0.0.0:8889` Tip: If you plan to only use the development server, you will need to setup whitenoise (see note below). ### Step 2 - Setup Backend Data, Automated Rebalancing and HTLC Stream Data @@ -92,16 +92,13 @@ Alternatively, you may also make your own task for these files with your preferr 2. Pull the new files `git pull` 3. Migrate any database changes `.venv/bin/python manage.py migrate` -### Nginx Webserver -If you would like to serve the dashboard at all times, it is recommended to setup a proper production webserver to host the site. -A bash script has been included to help aide in the setup of a nginx webserver. `sudo bash nginx.sh` - ### Notes 1. If you are not using the default settings for LND or you would like to run a LND instance on a network other than `mainnet` you can use the correct flags in step 6 (see `initialize.py --help`) or you can edit the variables directly in `lndg/lndg/settings.py`. 2. Some systems have a hard time serving static files (docker/macOs) and installing whitenoise and configuring it can help solve this issue. You can use `initialize.py -wn` to setup whitenoise and install it with `.venv/bin/pip install whitenoise`. 3. If you want to recreate a settings file, delete it from `lndg/lndg/settings.py` and rerun. `initialize.py` 4. If you plan to run this site continuously, consider setting up a proper web server to host it (see Nginx below). 5. You can manage your login credentials from the admin page. Example: `lndg.local/lndg-admin` +6. If you have issues reaching the site, verify the firewall is open on port 8889 where LNDg is running ### Setup lndg initialize.py options 1. `-ip` or `--nodeip` - Accepts only this host IP to serve the LNDg page - default: `*` @@ -115,6 +112,9 @@ A bash script has been included to help aide in the setup of a nginx webserver. 9. `-dx` or `--debug` - Setup the django site in debug mode - default: `False` 10. `-pw` or `--adminpw` Setup a custom admin password - default: `Randomized` +### Using A Webserver +You can serve the dashboard at all times using a webserver instead of the development server. Using a webserver will serve your static files and installing whitenoise is not required when running in this manner. Any webserver can be used to host the site if configured properly. A bash script has been included to help aide in the setup of a nginx webserver. `sudo bash nginx.sh` + ## Key Features ### API Backend The following data can be accessed at the /api endpoint: @@ -179,17 +179,22 @@ If you want a channel not to be picked for rebalancing (i.e. it is already full ## Preview Screens ### Main Dashboard -![image](https://user-images.githubusercontent.com/38626122/139308280-13b14393-c5f0-4e2a-8acc-9d87f5c83684.png) -![image](https://user-images.githubusercontent.com/38626122/137809328-c64c038b-8dbb-40a2-aeb3-a1bae5554d7a.png) -![image](https://user-images.githubusercontent.com/38626122/137809356-ec46193a-478c-424b-a184-2b15cfbb5c52.png) -![image](https://user-images.githubusercontent.com/38626122/137809433-b363fff1-31b6-4b0e-80e9-1916ef0af052.png) -![image](https://user-images.githubusercontent.com/38626122/137809648-bb191ba9-b989-4325-95ac-d25a8333ae62.png) +![image](https://user-images.githubusercontent.com/38626122/148699177-d10d412e-641e-4676-acac-2047e7e2d7a6.png) +![image](https://user-images.githubusercontent.com/38626122/148699209-667936fd-c56f-484f-8dd4-75e052c8c14f.png) +![image](https://user-images.githubusercontent.com/38626122/148699224-efb70fcf-0b7e-45cf-bd98-de833b2cff88.png) +![image](https://user-images.githubusercontent.com/38626122/148699273-be470d86-e76c-4935-8337-2b9737aed73e.png) +![image](https://user-images.githubusercontent.com/38626122/148699286-0b1d2c13-191a-4c6c-99ae-ce3d8b8ac64d.png) ![image](https://user-images.githubusercontent.com/38626122/137809583-db743233-25c1-4d3e-aaec-2a7767de2c9f.png) -### Peers, Balances, Routes and Pending HTLCs All Open In Separate Screens +### Peers, Balances, Routes, Keysends and Pending HTLCs All Open In Separate Screens ![image](https://user-images.githubusercontent.com/38626122/137809809-1ed40cfb-9d12-447a-8e5e-82ae79605895.png) ![image](https://user-images.githubusercontent.com/38626122/137810021-4f69dcb0-5fce-4062-bc49-e75f5dd0feda.png) ![image](https://user-images.githubusercontent.com/38626122/137809882-4a87f86d-290c-456e-9606-ed669fd98561.png) +![image](https://user-images.githubusercontent.com/38626122/148699417-bd9fbb49-72f5-4c3f-811f-e18c990a06ba.png) + +### Suggests Peers To Open With and Rebalancer Actions To Take +![image](https://user-images.githubusercontent.com/38626122/148699445-88efeacd-3cfc-429c-91d8-3a52ee633195.png) +![image](https://user-images.githubusercontent.com/38626122/148699467-62ebbd7d-9f36-4707-88fd-62f2cc2a5506.png) ### Browsable API at `/api` (json format available with url appended with `?format=json`) ![image](https://user-images.githubusercontent.com/38626122/137810278-7f38ac5b-8932-4953-aa4c-9c29d66dce0c.png) diff --git a/gui/migrations/0016_auto_20220106_1026.py b/gui/migrations/0016_auto_20220106_1026.py new file mode 100644 index 00000000..aefe4370 --- /dev/null +++ b/gui/migrations/0016_auto_20220106_1026.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.7 on 2022-01-06 10:26 + +from django.db import migrations, models + +def update_cost_to(apps, schedma_editor): + payments = apps.get_model('gui', 'payments') + hops = apps.get_model('gui', 'paymenthops') + for payment in payments.objects.filter(status=2).iterator(): + cost_to = 0 + for hop in hops.objects.filter(payment_hash=payment.payment_hash).order_by('step'): + hop.cost_to = round(cost_to, 3) + hop.save() + cost_to += hop.fee + +def revert_cost_to(apps, schedma_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0015_invoices_index'), + ] + + operations = [ + migrations.AddField( + model_name='channels', + name='num_updates', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='paymenthops', + name='cost_to', + field=models.FloatField(default=0), + ), + migrations.RunPython(update_cost_to, revert_cost_to), + migrations.AlterField( + model_name='paymenthops', + name='cost_to', + field=models.FloatField(), + ), + ] diff --git a/gui/migrations/0017_autopilot.py b/gui/migrations/0017_autopilot.py new file mode 100644 index 00000000..e46fcf12 --- /dev/null +++ b/gui/migrations/0017_autopilot.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.7 on 2022-01-06 20:37 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0016_auto_20220106_1026'), + ] + + operations = [ + migrations.CreateModel( + name='Autopilot', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('chan_id', models.IntegerField()), + ('peer_alias', models.CharField(max_length=32)), + ('setting', models.CharField(max_length=20)), + ('old_value', models.IntegerField()), + ('new_value', models.IntegerField()), + ], + ), + ] diff --git a/gui/models.py b/gui/models.py index c9f93bf9..b0a7d0c3 100644 --- a/gui/models.py +++ b/gui/models.py @@ -26,6 +26,7 @@ class PaymentHops(models.Model): node_pubkey = models.CharField(max_length=66) amt = models.FloatField() fee = models.FloatField() + cost_to = models.FloatField() class Meta: app_label = 'gui' unique_together = (('payment_hash', 'attempt_id', 'step'),) @@ -68,6 +69,7 @@ class Channels(models.Model): unsettled_balance = models.BigIntegerField() local_commit = models.IntegerField() local_chan_reserve = models.IntegerField() + num_updates = models.IntegerField() initiator = models.BooleanField() alias = models.CharField(max_length=32) local_base_fee = models.IntegerField() @@ -146,5 +148,15 @@ class FailedHTLCs(models.Model): wire_failure = models.IntegerField() failure_detail = models.IntegerField() missed_fee = models.FloatField() + class Meta: + app_label = 'gui' + +class Autopilot(models.Model): + timestamp = models.DateTimeField(default=timezone.now) + chan_id = models.IntegerField() + peer_alias = models.CharField(max_length=32) + setting = models.CharField(max_length=20) + old_value = models.IntegerField() + new_value = models.IntegerField() class Meta: app_label = 'gui' \ No newline at end of file diff --git a/gui/templates/autopilot.html b/gui/templates/autopilot.html new file mode 100644 index 00000000..bec59561 --- /dev/null +++ b/gui/templates/autopilot.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block content %} +{% load humanize %} +{% if autopilot %} +
+

Autopilot Logs

+ + + + + + + + + + {% for log in autopilot %} + + + + + + + + + {% endfor %} +
TimestampChannel IDPeer AliasSettingOld ValueNew Value
{{ log.timestamp|naturaltime }}{{ log.chan_id }}{{ log.peer_alias }}{{ log.setting }}{{ log.old_value }}{{ log.new_value }}
+
+{% endif %} +{% if not autopilot %} +
+

No autopilot logs to see here yet!

+
Experimental. Advanced users may activate via api.
+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/base.html b/gui/templates/base.html index 6ec852dc..1c39d537 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -28,7 +28,7 @@

My Lnd Overview

diff --git a/gui/templates/home.html b/gui/templates/home.html index 80c73a7f..3949cf53 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -3,8 +3,9 @@ {% load humanize %}

{{ node_info.alias }} | {{ node_info.identity_pubkey }}

-

Capacity: {{ capacity|intcomma }} | Active Channels: {{ node_info.num_active_channels }} | Peers: {{ node_info.num_peers }} | {% for info in node_info.chains %}{{ info }}{% endfor %} | {{ node_info.block_height }} | {{ node_info.block_hash }}

+

Capacity: {{ capacity|intcomma }} | Active Channels: {{ node_info.num_active_channels }} / {{ total_channels }} | Peers: {{ node_info.num_peers }} | DB Size: {{ db_size }} GB

Public Address: {% for info in node_info.uris %}{{ info }} | {% endfor %}

+

Lnd sync: {{ node_info.synced_to_graph }} | chain sync: {{ node_info.synced_to_chain }} | {% for info in node_info.chains %}{{ info }}{% endfor %} | {{ node_info.block_height }} | {{ node_info.block_hash }}

Wallet Balance: {{ balances.total_balance|intcomma }} | Confirmed Wallet Balance: {{ balances.confirmed_balance|intcomma }} | Unconfirmed Wallet Balance: {{ balances.unconfirmed_balance|intcomma }} | Details

@@ -22,7 +23,7 @@

7-Day Routed: {{ routed_7day|intcomma }} | Value: {{ routed_7day_amt|intcomm

Inbound Liquidity: {{ inbound|intcomma }} | Outbound Liquidity: {{ outbound|intcomma }} | Liquidity Ratio: {{ liq_ratio }}%

Balance In Limbo: {{ limbo_balance|intcomma }} | Unsettled Liquidity: {{ unsettled|intcomma }} | Pending HTLCs: {{ pending_htlc_count }}

-

Suggested New Peers | Suggested AR Actions

+

Suggested New Peers | Suggested AR Actions | Autopilot Logs

{% if active_channels %}
@@ -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

+ + + + + + + + {% for keysend in keysends %} + + + + + + + {% endfor %} +
Settle DateChannel In AliasAmountMessage
{{ keysend.settle_date|naturaltime }}{{ keysend.chan_in_alias }}{{ keysend.amt_paid|intcomma }}{{ keysend.message }}
+
+{% 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])