diff --git a/.gitignore b/.gitignore index d1abb459..f012f00e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ db.sqlite3 lndg/settings.py frontend node_modules -lndg-admin.txt \ No newline at end of file +lndg-admin.txt +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 994a2fd4..f8968b03 100644 --- a/README.md +++ b/README.md @@ -127,24 +127,40 @@ Alternatively, you may also make your own task for these files with your preferr 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: -`payments` `paymenthops` `invoices` `forwards` `onchain` `peers` `channels` `rebalancer` `settings` `pendinghtlcs` `failedhtlcs` +### Suggests New Peers +LNDg will make suggestions for new peers to open channels to based on your node's successful routing history. +#### There are two unique values in LNDg: +1. Volume Score - A score based upon both the count of transactions and the volume of transactions routed through the peer +2. Savings By Volume (ppm) - The amount of sats you could have saved during rebalances if you were peered directly with this node over the total amount routed through the peer -### Peer Reconnection -LNDg will automatically try to resolve any channels that are seen as inactive, no more than every 3 minutes per peer. +### Channel Performance Metrics +#### LNDg will aggregate your payment and forwarding data to provide the following metrics: +1. Outbound Flow Details - This shows the amount routed outbound next to the amount rebalanced in +2. Revenue Details - This shows the revenue earned on the left, the profit (revenue - cost) in the middle and the assisted revenue (amount earned due to this channel's inbound flow) on the right +3. Inbound Flow Details - This shows the amount routed inbound next to the amount rebalanced out +4. Updates - This is the number of updates the channel has had and is directly correlated to the space it takes up in channel.db -### Suggests New Peers -LNDg will make suggestions for new peers to open channels to based on your node's successful routing history. +### Password Protected Login +The initial login username is `lndg-admin` but can be easily modified by going to the page found here: `/lndg-admin` ### Suggests AR Actions LNDg will make suggestions for actions to take around Auto-Rebalancing. +### AR-Autopilot Setting +LNDg will automatically act upon the suggestions it makes on the Suggests AR Actions page. + ### HTLC Failure Stream LNDg will listen for failure events in your htlc stream and record them to the dashboard when they occur. -### Auto-Rebalancer -Here are some notes to help you get started using the Auto-Rebalancer (AR). +### API Backend +The following data can be accessed at the /api endpoint: +`payments` `paymenthops` `invoices` `forwards` `onchain` `peers` `channels` `rebalancer` `settings` `pendinghtlcs` `failedhtlcs` + +### Peer Reconnection +LNDg will automatically try to resolve any channels that are seen as inactive, no more than every 3 minutes per peer. + +## Auto-Rebalancer +### Here are some notes to help you get started using the Auto-Rebalancer (AR). The objective of the Auto-Rebalancer is to "refill" the liquidity on the local side (i.e. OUTBOUND) of profitable and lucarative channels. So that, when a forward comes in from another node there is always enough liquidity to route the payment and in return collect the desired routing fees. @@ -179,10 +195,10 @@ The objective of the Auto-Rebalancer is to "refill" the liquidity on the local s ``` Enabled: 1 Target Amount (%): 0.03 - Target Time (min): 5 + Target Time (min): 3 Target Outbound Above (%): 0.4 - Global Max Fee Rate (ppm): 200 - Max Cost (%): 0.5 + Global Max Fee Rate (ppm): 500 + Max Cost (%): 0.6 ``` 3. Go to section Last 10 Rebalance Requests - that will show the list of the rebalancing queue and status. @@ -197,7 +213,8 @@ If you want a channel not to be picked for rebalancing (i.e. it is already full ![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, Keysends and Pending HTLCs All Open In Separate Screens +### Channel Performance, Peers, Balances, Routes, Keysends and Pending HTLCs All Open In Separate Screens +![image](https://user-images.githubusercontent.com/38626122/150556928-bb8772fb-14c4-4b7a-865e-a8350aac7f83.png) ![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) diff --git a/gui/forms.py b/gui/forms.py index cd568966..1948ab92 100644 --- a/gui/forms.py +++ b/gui/forms.py @@ -70,7 +70,22 @@ class AutoRebalanceForm(forms.Form): fee_rate = forms.IntegerField(label='fee_rate', required=False) outbound_percent = forms.FloatField(label='outbound_percent', required=False) max_cost = forms.FloatField(label='max_cost', required=False) + autopilot = forms.IntegerField(label='autopilot', required=False) -class ARTarget(forms.Form): +updates_channel_codes = [ + (0, 'base_fee'), + (1, 'fee_rate'), + (2, 'ar_amt_target'), + (3, 'ar_in_target'), + (4, 'ar_out_target'), + (5, 'ar_enabled'), +] + +class UpdateChannel(forms.Form): chan_id = forms.IntegerField(label='chan_id') - ar_target = forms.IntegerField(label='ar_target') \ No newline at end of file + target = forms.IntegerField(label='target') + update_target = forms.ChoiceField(label='update_target', choices=updates_channel_codes) + +class UpdateSetting(forms.Form): + key = forms.CharField(label='setting', max_length=20) + value = forms.CharField(label='value', max_length=50) \ No newline at end of file diff --git a/gui/migrations/0018_auto_20220114_2218.py b/gui/migrations/0018_auto_20220114_2218.py new file mode 100644 index 00000000..13604c59 --- /dev/null +++ b/gui/migrations/0018_auto_20220114_2218.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.7 on 2022-01-14 22:18 + +from django.db import migrations, models +from django.db.models import F, IntegerField +from django.db.models.functions import Round + +def update_defaults(apps, schedma_editor): + channels = apps.get_model('gui', 'channels') + settings = apps.get_model('gui', 'localsettings') + try: + if settings.objects.filter(key='AR-Target%').exists(): + ar_amt_target = float(settings.objects.filter(key='AR-Target%')[0].value) + else: + ar_amt_target = 0.05 + channels.objects.all().update(ar_amt_target=Round(F('capacity')*ar_amt_target, output_field=IntegerField())) + except Exception as e: + print('Migration step failed:', str(e)) + try: + if settings.objects.filter(key='AR-Outbound%').exists(): + ar_out_target = float(settings.objects.filter(key='AR-Outbound%')[0].value) + else: + ar_out_target = 0.75 + channels.objects.all().update(ar_out_target=ar_out_target*100) + except Exception as e: + print('Migration step failed:', str(e)) + +def revert_defaults(apps, schedma_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0017_autopilot'), + ] + + operations = [ + migrations.RenameField( + model_name='channels', + old_name='ar_target', + new_name='ar_in_target', + ), + migrations.AddField( + model_name='channels', + name='ar_amt_target', + field=models.BigIntegerField(default=100000), + ), + migrations.AddField( + model_name='channels', + name='ar_out_target', + field=models.IntegerField(default=75), + ), + migrations.RunPython(update_defaults, revert_defaults), + ] diff --git a/gui/models.py b/gui/models.py index b0a7d0c3..80df78d8 100644 --- a/gui/models.py +++ b/gui/models.py @@ -79,7 +79,27 @@ class Channels(models.Model): is_active = models.BooleanField() is_open = models.BooleanField() auto_rebalance = models.BooleanField(default=False) - ar_target = models.IntegerField(default=100) + ar_amt_target = models.BigIntegerField(default=100000) + ar_in_target = models.IntegerField(default=100) + ar_out_target = models.IntegerField(default=75) + + def save(self, *args, **kwargs): + if not self.ar_out_target: + if LocalSettings.objects.filter(key='AR-Outbound%').exists(): + outbound_setting = float(LocalSettings.objects.filter(key='AR-Outbound%')[0].value) + else: + LocalSettings(key='AR-Outbound%', value='0.75').save() + outbound_setting = 0.75 + self.ar_out_target = int(outbound_setting * 100) + if not self.ar_amt_target: + if LocalSettings.objects.filter(key='AR-Target%').exists(): + amt_setting = float(LocalSettings.objects.filter(key='AR-Target%')[0].value) + else: + LocalSettings(key='AR-Target%', value='0.05').save() + amt_setting = 0.05 + self.ar_amt_target = int(amt_setting * self.capacity) + super(Channels, self).save(*args, **kwargs) + class Meta: app_label = 'gui' diff --git a/gui/serializers.py b/gui/serializers.py index 18652087..d5d8d7b5 100644 --- a/gui/serializers.py +++ b/gui/serializers.py @@ -40,6 +40,7 @@ class ChannelSerializer(serializers.HyperlinkedModelSerializer): remote_fee_rate = serializers.ReadOnlyField() is_active = serializers.ReadOnlyField() is_open = serializers.ReadOnlyField() + num_updates = serializers.ReadOnlyField() class Meta: model = Channels exclude = [] diff --git a/gui/templates/action_list.html b/gui/templates/action_list.html index 07eee150..2c1d481c 100644 --- a/gui/templates/action_list.html +++ b/gui/templates/action_list.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - Actions{% endblock %} {% block content %} {% load humanize %} {% if action_list %} @@ -24,8 +25,8 @@

Suggested Action List

{% for channel in action_list %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {{ channel.alias }} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} ({{ channel.outbound_percent }}%)
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
@@ -38,19 +39,13 @@

Suggested Action List

{{ channel.remote_fee_rate|intcomma }} {{ channel.remote_base_fee|intcomma }} - {% if channel.auto_rebalance == True %} -
+ {% csrf_token %} - + + +
- {% else %} -
- {% csrf_token %} - - -
- {% endif %} {{ channel.output }} diff --git a/gui/templates/advanced.html b/gui/templates/advanced.html new file mode 100644 index 00000000..6a85bdc0 --- /dev/null +++ b/gui/templates/advanced.html @@ -0,0 +1,131 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Advanced{% endblock %} +{% block content %} +{% load humanize %} +{% if channels %} +
+

Advanced Channel Settings

+
+ + + + + + + + + + + + + + + + + + + {% for channel in channels %} + + + + + + + + + + + + + + + + + + {% endfor %} +
Channel IDPeer AliasCapacityOutbound LiquidityInbound LiquidityoRateoBaseiRateiBaseTarget AmtoTarget%iTarget%ARActive
{{ channel.chan_id }}{{ channel.alias }}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }} ({{ channel.out_percent }}%)
{% if channel.in_percent == 0 %}
{% elif channel.out_percent == 0 %}
{% else %}
{% endif %}
{{ channel.remote_balance|intcomma }} ({{ channel.in_percent }}%) +
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
{{ channel.remote_fee_rate|intcomma }}{{ channel.remote_base_fee|intcomma }} +
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + + +
+
{{ channel.is_active }}
+
+
+{% endif %} +{% if not channels %} +
+

You dont have any channels to setup yet!

+
+{% endif %} +{% if local_settings %} +
+

Update Local Settings

+ + + + + + {% for settings in local_settings %} + + + + + {% endfor %} +
KeyValue
{{ settings.key }} +
+ {% csrf_token %} + {% if settings.key|slice:"-1:" == '%' %} + + {% elif settings.key == 'AR-Time' %} + + {% elif settings.key == 'AR-MaxFeeRate' %} + + {% elif settings.key|slice:":3" == 'AR-' %} + + {% else %} + + {% endif %} + +
+
+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/autopilot.html b/gui/templates/autopilot.html index bec59561..f664b1e0 100644 --- a/gui/templates/autopilot.html +++ b/gui/templates/autopilot.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - Autopilot{% endblock %} {% block content %} {% load humanize %} {% if autopilot %} @@ -29,7 +30,7 @@

Autopilot Logs

{% if not autopilot %}

No autopilot logs to see here yet!

-
Experimental. Advanced users may activate via api.
+
Experimental. This will allow LNDg to automatically act upon the suggestions found here.
{% endif %} {% endblock %} \ No newline at end of file diff --git a/gui/templates/balances.html b/gui/templates/balances.html index 4642477f..41c14eda 100644 --- a/gui/templates/balances.html +++ b/gui/templates/balances.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - Balances{% endblock %} {% block content %} {% load humanize %} {% if utxos %} @@ -13,9 +14,9 @@

Balances

{% for uxto in utxos %} - {{ uxto.address }} + {{ uxto.address }} {{ uxto.amount_sat|intcomma }} - {{ uxto.outpoint.txid_str }} + {{ uxto.outpoint.txid_str }} {{ uxto.confirmations|intcomma }} {% endfor %} @@ -35,9 +36,9 @@

Transactions

{% for transaction in transactions %} - {{ transaction.tx_hash }} + {{ transaction.tx_hash }} {{ transaction.amount }} - {{ transaction.block_height }} + {% if transaction.block_height == 0 %}{{ transaction.block_height }}{% else %}{{ transaction.block_height }}{% endif %} {{ transaction.fee }} {{ transaction.label }} @@ -45,4 +46,9 @@

Transactions

{% endif %} +{% if not utxos and not transactions %} +
+

No wallet transactions found!

+
+{% endif %} {% endblock %} \ No newline at end of file diff --git a/gui/templates/base.html b/gui/templates/base.html index 1c39d537..00522fb7 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -4,7 +4,7 @@ - LNDg + {% block title %}LNDg{% endblock %} {% load static %} {% load qr_code %} @@ -28,7 +28,7 @@

My Lnd Overview

diff --git a/gui/templates/channels.html b/gui/templates/channels.html new file mode 100644 index 00000000..8c2555ba --- /dev/null +++ b/gui/templates/channels.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Channels{% endblock %} +{% block content %} +{% load humanize %} +{% if channels %} +
+

Channel Performance

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% for channel in channels %} + + + + + + + + + + + + + + + + + + {% endfor %} +
7-Day Activity And Revenue 30-Day Activity And Revenue Local FeesPeer Fees Channel Health
Channel IDPeer AliasCapacityOutbound FlowOut [Profit] | InInbound FlowOutbound FlowOut [Profit] | InInbound FlowRateBaseRateBaseUpdatesOpener
{{ channel.chan_id }}{{ channel.alias }}{{ channel.capacity }} M{{ channel.amt_routed_out_7day|intcomma }} M ({{ channel.routed_out_7day }}) | {{ channel.amt_rebal_in_7day|intcomma }} M ({{ channel.rebal_in_7day }}){{ channel.revenue_7day|intcomma }} [{{ channel.profits_7day|intcomma }}] | {{ channel.revenue_assist_7day|intcomma }}{{ channel.amt_routed_in_7day|intcomma }} M ({{ channel.routed_in_7day }}) | {{ channel.amt_rebal_out_7day|intcomma }} M ({{ channel.rebal_out_7day }}){{ channel.amt_routed_out_30day|intcomma }} M ({{ channel.routed_out_30day }}) | {{ channel.amt_rebal_in_30day|intcomma }} M ({{ channel.rebal_in_30day }}){{ channel.revenue_30day|intcomma }} [{{ channel.profits_30day|intcomma }}] | {{ channel.revenue_assist_30day|intcomma }}{{ channel.amt_routed_in_30day|intcomma }} M ({{ channel.routed_in_30day }}) | {{ channel.amt_rebal_out_30day|intcomma }} M ({{ channel.rebal_out_30day }}){{ channel.local_fee_rate|intcomma }}{{ channel.local_base_fee|intcomma }}{{ channel.remote_fee_rate|intcomma }}{{ channel.remote_base_fee|intcomma }}{{ channel.num_updates|intcomma }}{{ channel.initiator }}
+
+
+{% endif %} +{% if not channels %} +
+

You dont have any channels to analyze yet!

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/failed_htlcs.html b/gui/templates/failed_htlcs.html new file mode 100644 index 00000000..a294ac14 --- /dev/null +++ b/gui/templates/failed_htlcs.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Failed HTLCs{% endblock %} +{% block content %} +{% load humanize %} +{% if failed_htlcs %} +
+

Last 150 Failed HTLCs

+ + + + + + + + + + + + + {% for failed_htlc in failed_htlcs %} + + + + + + + + + + + + {% endfor %} +
TimestampChan In IDChan Out IDChan In AliasChan Out AliasForward AmountPotential FeeHTLC FailureFailure Detail
{{ failed_htlc.timestamp|naturaltime }}{{ failed_htlc.chan_id_in }}{{ failed_htlc.chan_id_out }}{{ failed_htlc.chan_in_alias }}{{ failed_htlc.chan_out_alias }}{{ failed_htlc.amount|intcomma }}{{ failed_htlc.missed_fee|intcomma }}{% if failed_htlc.wire_failure == 15 %}Temporary Channel Failure{% elif failed_htlc.wire_failure == 18 %}Unknown Next Peer{% elif failed_htlc.wire_failure == 12 %}Fee Insufficient{% else %}{{ failed_htlc.wire_failure }}{% endif %}{% if failed_htlc.failure_detail == 1 %}---{% elif failed_htlc.failure_detail == 5 %}HTLC Exceeds Max{% elif failed_htlc.failure_detail == 6 %}Insufficient Balance{% elif failed_htlc.failure_detail == 20 %}Invalid Keysend{% elif failed_htlc.failure_detail == 22 %}Circular Route{% else %}{{ failed_htlc.failure_detail }}{% endif %}
+
+{% endif %} +{% if not failed_htlcs %} +
+

You dont have any failed HTLCs yet.

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/forwards.html b/gui/templates/forwards.html new file mode 100644 index 00000000..c3ee1b3e --- /dev/null +++ b/gui/templates/forwards.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Forwards{% endblock %} +{% block content %} +{% load humanize %} +{% if forwards %} +
+

Last 150 Forwards

+ + + + + + + + + + + + + {% for forward in forwards %} + + + + + + + + + + + + {% endfor %} +
TimestampAmount InAmount OutChannel In AliasChannel Out AliasChannel In IdChannel Out IdFees EarnedPPM Earned
{{ forward.forward_date|naturaltime }}{{ forward.amt_in|intcomma }}{{ forward.amt_out|intcomma }}{{ forward.chan_in_alias }}{{ forward.chan_out_alias }}{{ forward.chan_id_in }}{{ forward.chan_id_out }}{{ forward.fee }}{{ forward.ppm|intcomma }}
+
+{% endif %} +{% if not forwards %} +
+

You dont have any forwards yet.

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/home.html b/gui/templates/home.html index 3949cf53..0116e537 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -1,8 +1,9 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - Dashboard{% endblock %} {% block content %} {% load humanize %}
-

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

+

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

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 }}

@@ -23,7 +24,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 | Autopilot Logs

+

Suggested New Peers | Suggested AR Actions | Autopilot Logs | Channel Performance | Advanced Settings

{% if active_channels %}
@@ -46,13 +47,13 @@

Active Channels

iLife iRate iBase - iTarget% - AR + iTarget% + AR {% for channel in active_channels %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {{ channel.alias }} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} ({{ channel.outbound_percent }}%)
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
@@ -68,29 +69,24 @@

Active Channels

{{ channel.remote_base_fee|intcomma }} {% if channel.auto_rebalance == True %} -
+ {% csrf_token %} - + +
{% else %} --- {% endif %} - {% if channel.auto_rebalance == True %} -
- {% csrf_token %} - - -
- {% else %} -
- {% csrf_token %} - - -
- {% endif %} +
+ {% csrf_token %} + + + + +
{% endfor %} @@ -122,8 +118,8 @@

Inactive Channels

{% for channel in inactive_channels %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {{ channel.alias }} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }}
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
@@ -138,29 +134,24 @@

Inactive Channels

{{ channel.initiator }} {% if channel.auto_rebalance == True %} -
+ {% csrf_token %} - + +
{% else %} --- {% endif %} - {% if channel.auto_rebalance == True %} -
- {% csrf_token %} - - -
- {% else %} -
- {% csrf_token %} - - -
- {% endif %} +
+ {% csrf_token %} + + + + +
{% endfor %} @@ -182,9 +173,9 @@

Pending Open Channels

{% for channel in pending_open %} {% with pending_funding_txid=channel.channel.channel_point|slice:":-2" %} - {{ channel.channel.channel_point }} + {{ channel.channel.channel_point }} {% endwith %} - {{ channel.channel.remote_node_pub }} + {{ channel.channel.remote_node_pub }} {{ channel.channel.capacity|intcomma }} {{ channel.channel.local_balance|intcomma }} {{ channel.channel.remote_balance|intcomma }} @@ -210,9 +201,9 @@

Pending Close Channels

{% for channel in pending_closed %} {% with pending_funding_txid=channel.channel.channel_point|slice:":-2" %} - {{ channel.channel.channel_point }} + {{ channel.channel.channel_point }} {% endwith %} - {{ channel.channel.remote_node_pub }} + {{ channel.channel.remote_node_pub }} {{ channel.channel.capacity|intcomma }} {{ channel.channel.local_balance|intcomma }} {{ channel.channel.remote_balance|intcomma }} @@ -240,15 +231,15 @@

Pending Force Close Channels

{% for channel in pending_force_closed %} {% with pending_funding_txid=channel.channel.channel_point|slice:":-2" %} - {{ channel.channel.channel_point }} + {{ channel.channel.channel_point }} {% endwith %} - {{ channel.channel.remote_node_pub }} + {{ channel.channel.remote_node_pub }} {{ channel.channel.capacity|intcomma }} {{ channel.channel.local_balance|intcomma }} {{ channel.channel.remote_balance|intcomma }} {{ channel.limbo_balance|intcomma }} {{ channel.blocks_til_maturity|intcomma }} - {{ channel.closing_txid }} + {{ channel.closing_txid }} {% endfor %} @@ -270,9 +261,9 @@

Channels Waiting To Close

{% for channel in waiting_for_close %} {% with pending_funding_txid=channel.channel.channel_point|slice:":-2" %} - {{ channel.channel.channel_point }} + {{ channel.channel.channel_point }} {% endwith %} - {{ channel.channel.remote_node_pub }} + {{ channel.channel.remote_node_pub }} {{ channel.channel.capacity|intcomma }} {{ channel.channel.local_balance|intcomma }} {{ channel.channel.remote_balance|intcomma }} @@ -285,7 +276,7 @@

Channels Waiting To Close

{% endif %} {% if forwards %}
-

Last 10 Payments Routed

+

Last 10 Payments Routed

@@ -337,7 +328,7 @@

Last 10 Rebalance Requests

- + {% endfor %}
Timestamp {{ rebalance.value|intcomma }} {{ rebalance.fee_limit|intcomma }} {% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}Successful{% elif rebalance.status == 3 %}Timeout{% elif rebalance.status == 4 %}No Route{% elif rebalance.status == 5 %}Error{% elif rebalance.status == 6 %}Incorrect Payment Details{% elif rebalance.status == 7 %}Insufficient Balance{% elif rebalance.status == 400 %}Rebalancer Request Failed{% elif rebalance.status == 408 %}Rebalancer Request Timeout{% else %}{{ rebalance.status }}{% endif %}{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}Successful{% elif rebalance.status == 3 %}Timeout{% elif rebalance.status == 4 %}No Route{% elif rebalance.status == 5 %}Error{% elif rebalance.status == 6 %}Incorrect Payment Details{% elif rebalance.status == 7 %}Insufficient Balance{% elif rebalance.status == 400 %}Rebalancer Request Failed{% elif rebalance.status == 408 %}Rebalancer Request Timeout{% else %}{{ rebalance.status }}{% endif %}
@@ -345,7 +336,7 @@

Last 10 Rebalance Requests

{% endif %} {% if payments %}
-

Last 5 Payments Sent

+

Last 5 Payments Sent

@@ -376,7 +367,7 @@

Last 5 Payments Sent

{% endif %} {% if invoices %}
-

Last 5 Payments Received

+

Last 5 Payments Received

Timestamp
@@ -407,7 +398,7 @@

Last 5 Payments Received

{% endif %} {% if failed_htlcs %}
-

Last 10 Failed HTLCs

+

Last 10 Failed HTLCs

Created
@@ -467,9 +458,11 @@

Update Auto Rebalancer Settings

- + + + @@ -581,4 +574,4 @@

Update Peer Alias

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/invoices.html b/gui/templates/invoices.html new file mode 100644 index 00000000..b46fa047 --- /dev/null +++ b/gui/templates/invoices.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Invoices{% endblock %} +{% block content %} +{% load humanize %} +{% if invoices %} +
+

Last 150 Invoices

+
Timestamp
+ + + + + + + + + + + + {% for invoice in invoices %} + + + + + + + + + + + + {% endfor %} +
CreatedSettledPayment HashValueAmount PaidStateChannel In AliasChannel InKeysend
{{ invoice.creation_date|naturaltime }}{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}N/A{% endif %}{{ invoice.r_hash }}{{ invoice.value|add:"0"|intcomma }}{% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}N/A{% endif %}{% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %}{{ invoice.chan_in_alias }}{{ invoice.chan_in }}{% if invoice.keysend_preimage != None %}Yes{% else %}No{% endif %}
+
+{% endif %} +{% if not invoices %} +
+

You dont have any invoices yet.

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/keysends.html b/gui/templates/keysends.html index 4f1c0881..3104ab99 100644 --- a/gui/templates/keysends.html +++ b/gui/templates/keysends.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - Keysends{% endblock %} {% block content %} {% load humanize %} {% if keysends %} diff --git a/gui/templates/open_list.html b/gui/templates/open_list.html index af80cb27..7825e23a 100644 --- a/gui/templates/open_list.html +++ b/gui/templates/open_list.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - Opens{% endblock %} {% block content %} {% load humanize %} {% if open_list %} @@ -11,13 +12,13 @@

Suggested Open List

Successful Payments Routed Amount Routed Fees Paid - Effective PPM - Volume Score - Savings By Volume + Effective PPM + Volume Score + Savings By Volume {% for node in open_list %} - {{ node.node_pubkey }} + {{ node.node_pubkey }} {{ node.alias }} {{ node.count }} {{ node.amount|add:"0"|intcomma }} @@ -30,4 +31,9 @@

Suggested Open List

{% endif %} +{% if not open_list %} +
+

No potential peers can be calculated yet, try waiting until you have some payment data.

+
+{% endif %} {% endblock %} \ No newline at end of file diff --git a/gui/templates/payments.html b/gui/templates/payments.html new file mode 100644 index 00000000..48dc8173 --- /dev/null +++ b/gui/templates/payments.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Payments{% endblock %} +{% block content %} +{% load humanize %} +{% if payments %} +
+

Last 150 Payments

+ + + + + + + + + + + + + {% for payment in payments %} + + + + + + + + + + + + {% endfor %} +
TimestampHashValueFee PaidStatusChan Out AliasChan Out IDRouteKeysend
{{ payment.creation_date|naturaltime }}{{ payment.payment_hash }}{{ payment.value|add:"0"|intcomma }}{{ payment.fee|intcomma }}{% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %}{% if payment.status == 2 %}{{ payment.chan_out_alias }}{% else %}N/A{% endif %}{% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %}{% if payment.status == 2 %}Open{% else %}N/A{% endif %}{% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %}
+
+{% endif %} +{% if not payments %} +
+

You dont have any payments yet.

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/peers.html b/gui/templates/peers.html index c600a142..b6e797fa 100644 --- a/gui/templates/peers.html +++ b/gui/templates/peers.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - Peers{% endblock %} {% block content %} {% load humanize %} {% if peers %} @@ -14,7 +15,7 @@

Peers List

{% for peer in peers %} - {{ peer.pubkey }} + {{ peer.pubkey }} {{ peer.address }} {{ peer.inbound }} {{ peer.sat_sent|intcomma }} @@ -24,4 +25,9 @@

Peers List

{% endif %} +{% if not peers %} +
+

No connected peers found!

+
+{% endif %} {% endblock %} \ No newline at end of file diff --git a/gui/templates/pending_htlcs.html b/gui/templates/pending_htlcs.html index 9061a20a..ab591981 100644 --- a/gui/templates/pending_htlcs.html +++ b/gui/templates/pending_htlcs.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - HTLCs{% endblock %} {% block content %} {% load humanize %} {% if outgoing_htlcs %} @@ -55,4 +56,9 @@

Incoming HTLCs

{% endif %} +{% if not outgoing_htlcs and not incoming_htlcs %} +
+

No pending HTLCs were found!

+
+{% endif %} {% endblock %} \ No newline at end of file diff --git a/gui/templates/route.html b/gui/templates/route.html index 2affa7fb..dce393c5 100644 --- a/gui/templates/route.html +++ b/gui/templates/route.html @@ -1,6 +1,8 @@ {% extends "base.html" %} +{% block title %} {{ block.super }} - Routes{% endblock %} {% block content %} {% load humanize %} +{% if route %}

Route For Payment: {{ payment_hash }}

@@ -8,22 +10,30 @@

Route For Payment: {{ payment_hash }}

+ + - {% for hop in route %} + + - {% endfor %}
Step Amount FeePPMCost To Alias Channel ID Channel CapacityCost To
{{ hop.step }} {{ hop.amt }} {{ hop.fee }}{{ hop.ppm }}{{ hop.cost_to }} {{ hop.alias }} {{ hop.chan_id }} {{ hop.chan_capacity|intcomma }}{{ hop.cost_to }}
+{% endif %} +{% if not route %} +
+

A route was not found for this payment hash!

+
+{% endif %} {% endblock %} \ No newline at end of file diff --git a/gui/urls.py b/gui/urls.py index 549726b8..a4f78d38 100644 --- a/gui/urls.py +++ b/gui/urls.py @@ -22,6 +22,10 @@ path('peers', views.peers, name='peers'), path('balances', views.balances, name='balances'), path('pending_htlcs', views.pending_htlcs, name='pending-htlcs'), + path('failed_htlcs', views.failed_htlcs, name='failed-htlcs'), + path('payments', views.payments, name='payments'), + path('invoices', views.invoices, name='invoices'), + path('forwards', views.forwards, name='forwards'), path('openchannel/', views.open_channel_form, name='open-channel-form'), path('closechannel/', views.close_channel_form, name='close-channel-form'), path('connectpeer/', views.connect_peer_form, name='connect-peer-form'), @@ -30,11 +34,14 @@ path('rebalancer/', views.rebalance, name='rebalancer'), path('updatechanpolicy/', views.update_chan_policy, name='updatechanpolicy'), path('autorebalance/', views.auto_rebalance, name='auto-rebalance'), - path('ar_target/', views.ar_target, name='ar-target'), + path('update_channel/', views.update_channel, name='update-channel'), + path('update_setting/', views.update_setting, name='update-setting'), 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('channels/', views.channels, name='channels'), path('autopilot/', views.autopilot, name='autopilot'), + path('advanced/', views.advanced, name='advanced'), 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'), diff --git a/gui/views.py b/gui/views.py index 87079839..ce4c93ca 100644 --- a/gui/views.py +++ b/gui/views.py @@ -1,6 +1,6 @@ from django.contrib import messages from django.shortcuts import get_object_or_404, render, redirect -from django.db.models import Sum, IntegerField, Count +from django.db.models import Sum, IntegerField, Count, F from django.db.models.functions import Round from django.contrib.auth.decorators import login_required from django.conf import settings @@ -8,7 +8,7 @@ from rest_framework import viewsets 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 .forms import OpenChannelForm, CloseChannelForm, ConnectPeerForm, AddInvoiceForm, RebalancerForm, ChanPolicyForm, UpdateChannel, UpdateSetting, AutoRebalanceForm 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 @@ -16,6 +16,23 @@ from .lnd_deps.lnd_connect import lnd_connect from lndg.settings import LND_NETWORK, LND_DIR_PATH from os import path +from pandas import DataFrame + +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://1ml.com').save() + graph_links = 'https://1ml.com' + 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 @login_required(login_url='/lndg-admin/login/?next=/') def home(request): @@ -41,26 +58,36 @@ def home(request): total_received = 0 if total_invoices == 0 else invoices.aggregate(Sum('amt_paid'))['amt_paid__sum'] #Get recorded forwarding events forwards = Forwards.objects.all().annotate(amt_in=Sum('amt_in_msat')/1000).annotate(amt_out=Sum('amt_out_msat')/1000).annotate(ppm=Round((Sum('fee')*1000000000)/Sum('amt_out_msat'), output_field=IntegerField())).order_by('-id') - total_forwards = forwards.count() - total_value_forwards = 0 if total_forwards == 0 else int(forwards.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000) - total_earned = 0 if total_forwards == 0 else forwards.aggregate(Sum('fee'))['fee__sum'] + forwards_df = DataFrame.from_records(forwards.values()) + total_forwards = forwards_df.shape[0] + total_value_forwards = 0 if total_forwards == 0 else int(forwards_df['amt_out_msat'].sum()/1000) + total_earned = 0 if total_forwards == 0 else forwards_df['fee'].sum() + forwards_df_in_sum = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_in', as_index=True).sum() + forwards_df_out_sum = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_out', as_index=True).sum() + forwards_df_in_count = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_in', as_index=True).count() + forwards_df_out_count = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_out', as_index=True).count() #Get current active channels active_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')).order_by('outbound_percent') total_capacity = 0 if active_channels.count() == 0 else active_channels.aggregate(Sum('capacity'))['capacity__sum'] total_inbound = 0 if total_capacity == 0 else active_channels.aggregate(Sum('remote_balance'))['remote_balance__sum'] total_outbound = 0 if total_capacity == 0 else active_channels.aggregate(Sum('local_balance'))['local_balance__sum'] total_unsettled = 0 if total_capacity == 0 else active_channels.aggregate(Sum('unsettled_balance'))['unsettled_balance__sum'] - detailed_active_channels = [] filter_7day = datetime.now() - timedelta(days=7) - routed_7day = forwards.filter(forward_date__gte=filter_7day).count() - routed_7day_amt = 0 if routed_7day == 0 else int(forwards.filter(forward_date__gte=filter_7day).aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000) - total_earned_7day = 0 if routed_7day == 0 else forwards.filter(forward_date__gte=filter_7day).aggregate(Sum('fee'))['fee__sum'] + forwards_df_7d = DataFrame.from_records(forwards.filter(forward_date__gte=filter_7day).values()) + forwards_df_in_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_in', as_index=True).sum() + forwards_df_out_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_out', as_index=True).sum() + 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() + routed_7day = forwards_df_7d.shape[0] + routed_7day_amt = 0 if routed_7day == 0 else int(forwards_df_7d['amt_out_msat'].sum()/1000) + total_earned_7day = 0 if routed_7day == 0 else forwards_df_7d['fee'].sum() payments_7day = payments.filter(status=2).filter(creation_date__gte=filter_7day) payments_7day_amt = 0 if payments_7day.count() == 0 else payments_7day.aggregate(Sum('value'))['value__sum'] total_7day_fees = 0 if payments_7day.count() == 0 else payments_7day.aggregate(Sum('fee'))['fee__sum'] pending_htlcs = PendingHTLCs.objects.all() pending_htlc_count = pending_htlcs.count() pending_outbound = 0 if pending_htlcs.filter(incoming=False).count() == 0 else pending_htlcs.filter(incoming=False).aggregate(Sum('amount'))['amount__sum'] + detailed_active_channels = [] for channel in active_channels: detailed_channel = {} detailed_channel['remote_pubkey'] = channel.remote_pubkey @@ -79,17 +106,17 @@ def home(request): detailed_channel['output_index'] = channel.output_index detailed_channel['outbound_percent'] = int(round(channel.outbound_percent/10, 0)) detailed_channel['inbound_percent'] = int(round(channel.inbound_percent/10, 0)) - detailed_channel['routed_in'] = forwards.filter(chan_id_in=channel.chan_id).count() - detailed_channel['routed_out'] = forwards.filter(chan_id_out=channel.chan_id).count() - detailed_channel['amt_routed_in'] = 0 if detailed_channel['routed_in'] == 0 else int(forwards.filter(chan_id_in=channel.chan_id).aggregate(Sum('amt_in_msat'))['amt_in_msat__sum']/10000000)/100 - detailed_channel['amt_routed_out'] = 0 if detailed_channel['routed_out'] == 0 else int(forwards.filter(chan_id_out=channel.chan_id).aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/10000000)/100 - detailed_channel['routed_in_7day'] = forwards.filter(forward_date__gte=filter_7day).filter(chan_id_in=channel.chan_id).count() - detailed_channel['routed_out_7day'] = forwards.filter(forward_date__gte=filter_7day).filter(chan_id_out=channel.chan_id).count() - detailed_channel['amt_routed_in_7day'] = 0 if detailed_channel['routed_in_7day'] == 0 else int(forwards.filter(forward_date__gte=filter_7day).filter(chan_id_in=channel.chan_id).aggregate(Sum('amt_in_msat'))['amt_in_msat__sum']/10000000)/100 - detailed_channel['amt_routed_out_7day'] = 0 if detailed_channel['routed_out_7day'] == 0 else int(forwards.filter(forward_date__gte=filter_7day).filter(chan_id_out=channel.chan_id).aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/10000000)/100 + detailed_channel['routed_in'] = forwards_df_in_count.loc[channel.chan_id].amt_out_msat if (forwards_df_in_count.index == channel.chan_id).any() else 0 + detailed_channel['routed_out'] = forwards_df_out_count.loc[channel.chan_id].amt_out_msat if (forwards_df_out_count.index == channel.chan_id).any() else 0 + detailed_channel['amt_routed_in'] = int(forwards_df_in_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_in_sum.index == channel.chan_id).any() else 0 + detailed_channel['amt_routed_out'] = int(forwards_df_out_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_out_sum.index == channel.chan_id).any() else 0 + detailed_channel['routed_in_7day'] = forwards_df_in_7d_count.loc[channel.chan_id].amt_out_msat if (forwards_df_in_7d_count.index == channel.chan_id).any() else 0 + detailed_channel['routed_out_7day'] = forwards_df_out_7d_count.loc[channel.chan_id].amt_out_msat if (forwards_df_out_7d_count.index == channel.chan_id).any() else 0 + detailed_channel['amt_routed_in_7day'] = int(forwards_df_in_7d_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_in_7d_sum.index == channel.chan_id).any() else 0 + detailed_channel['amt_routed_out_7day'] = int(forwards_df_out_7d_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_out_7d_sum.index == channel.chan_id).any() else 0 detailed_channel['htlc_count'] = pending_htlcs.filter(chan_id=channel.chan_id).count() detailed_channel['auto_rebalance'] = channel.auto_rebalance - detailed_channel['ar_target'] = channel.ar_target + detailed_channel['ar_in_target'] = channel.ar_in_target detailed_active_channels.append(detailed_channel) #Get current inactive channels inactive_channels = Channels.objects.filter(is_active=False, is_open=True).annotate(outbound_percent=(Sum('local_balance')*100)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*100)/Sum('capacity')).order_by('-local_fee_rate').order_by('outbound_percent') @@ -103,7 +130,7 @@ def home(request): #Get list of recent rebalance requests rebalances = Rebalancer.objects.all().order_by('-requested') total_channels = node_info.num_active_channels + node_info.num_inactive_channels - local_settings = LocalSettings.objects.all() + local_settings = LocalSettings.objects.filter(key__contains='AR-') try: db_size = round(path.getsize(path.expanduser(LND_DIR_PATH + '/data/graph/' + LND_NETWORK + '/channel.db'))*0.000000001, 3) except: @@ -120,7 +147,7 @@ def home(request): 'invoices': invoices[:6], 'total_received': total_received, 'total_invoices': total_invoices, - 'forwards': forwards[:15], + 'forwards': forwards_df.head(15).to_dict(orient='records'), 'earned': int(total_earned), 'total_forwards': total_forwards, 'total_value_forwards': total_value_forwards, @@ -158,19 +185,111 @@ def home(request): '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 '', + 'graph_links': graph_links(), + 'network_links': network_links(), 'db_size': db_size } return render(request, 'home.html', context) else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') +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).filter(creation_date__gte=filter_30day).filter(payment_hash__in=Invoices.objects.filter(state=1).filter(settle_date__gte=filter_30day).values_list('r_hash')) + invoices = Invoices.objects.filter(state=1).filter(settle_date__gte=filter_30day).filter(r_hash__in=payments.values_list('payment_hash')) + channels = Channels.objects.filter(is_open=True) + 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() + forwards_df_out_30d_sum = DataFrame() if forwards_df_30d.empty else forwards_df_30d.groupby('chan_id_out', as_index=True).sum() + forwards_df_in_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_in', as_index=True).sum() + forwards_df_out_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_out', as_index=True).sum() + 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() + payments_df_7d_sum = DataFrame() if payments_df_7d.empty else payments_df_7d.groupby('chan_out', as_index=True).sum() + 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() + invoices_df_7d_sum = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True).sum() + 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['capacity'] = channels_df.apply(lambda row: round(row.capacity/1000000, 1), 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: 0 if row['revenue_7day'] == 0 else row['revenue_7day'] - row['costs_7day'], axis=1) + channels_df['profits_30day'] = channels_df.apply(lambda row: 0 if row['revenue_30day'] == 0 else row['revenue_30day'] - row['costs_30day'], axis=1) + channels_df['open_block'] = channels_df.apply(lambda row: row.chan_id>>40, axis=1) + context = { + 'channels': channels_df.to_dict(orient='records'), + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'graph_links': graph_links(), + 'network_links': network_links() + } + return render(request, 'channels.html', context) + else: + return redirect('home') + +@login_required(login_url='/lndg-admin/login/?next=/') +def advanced(request): + if request.method == 'GET': + channels = Channels.objects.filter(is_open=True).annotate(outbound_percent=(Sum('local_balance')*1000)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*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) + context = { + 'channels': channels_df.to_dict(orient='records'), + 'local_settings': LocalSettings.objects.all(), + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'graph_links': graph_links(), + 'network_links': network_links() + } + return render(request, 'advanced.html', context) + else: + return redirect('home') + @login_required(login_url='/lndg-admin/login/?next=/') def route(request): if request.method == 'GET': payment_hash = request.GET.urlencode()[1:] context = { 'payment_hash': payment_hash, - 'route': PaymentHops.objects.filter(payment_hash=payment_hash) + 'route': PaymentHops.objects.filter(payment_hash=payment_hash).annotate(ppm=Round((Sum('fee')/Sum('amt'))*1000000, output_field=IntegerField())) } return render(request, 'route.html', context) else: @@ -180,7 +299,9 @@ def route(request): def peers(request): if request.method == 'GET': context = { - 'peers': Peers.objects.filter(connected=True) + 'peers': Peers.objects.filter(connected=True), + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'graph_links': graph_links() } return render(request, 'peers.html', context) else: @@ -192,7 +313,9 @@ def balances(request): stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) context = { 'utxos': stub.ListUnspent(ln.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')) + 'transactions': list(Onchain.objects.filter(block_height=0)) + list(Onchain.objects.exclude(block_height=0).order_by('-block_height')), + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network_links': network_links() } return render(request, 'balances.html', context) else: @@ -208,7 +331,9 @@ def suggested_opens(request): 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')).annotate(sum_cost_to=Sum('cost_to')/(Sum('amt')/1000000)).order_by('-score', 'ppm')[:21] context = { - 'open_list': open_list + 'open_list': open_list, + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'graph_links': graph_links() } return render(request, 'open_list.html', context) else: @@ -240,33 +365,36 @@ def suggested_actions(request): 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_target + result['ar_target'] = channel.ar_in_target if result['o7D'] > (result['i7D']*1.10) and result['outbound_percent'] > 75: - print('Case 1: Pass') + #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') + #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') + #print('Case 2: Enable AR') 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: - print('Case 3: Disable AR') + #print('Case 3: Disable AR') result['output'] = 'Disable AR' result['reason'] = 'o7D < i7D AND Outbound Liq > 75%' elif result['o7D'] < (result['i7D']*1.10) and result['inbound_percent'] > 75: - print('Case 4: Pass') + #print('Case 4: Pass') continue else: - print('Case 5: Pass') + #print('Case 5: Pass') continue if len(result) > 0: action_list.append(result) context = { - 'action_list': action_list + 'action_list': action_list, + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'graph_links': graph_links(), + 'network_links': network_links() } return render(request, 'action_list.html', context) else: @@ -283,6 +411,47 @@ def pending_htlcs(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') +def failed_htlcs(request): + if request.method == 'GET': + context = { + 'failed_htlcs': FailedHTLCs.objects.all().order_by('-timestamp')[:150], + } + return render(request, 'failed_htlcs.html', context) + else: + return redirect('home') + +@login_required(login_url='/lndg-admin/login/?next=/') +def payments(request): + if request.method == 'GET': + context = { + 'payments': Payments.objects.filter(status=2).order_by('-creation_date')[:150], + } + return render(request, 'payments.html', context) + else: + return redirect('home') + +@login_required(login_url='/lndg-admin/login/?next=/') +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') + +@login_required(login_url='/lndg-admin/login/?next=/') +def forwards(request): + if request.method == 'GET': + context = { + 'forwards': Forwards.objects.all().annotate(amt_in=Sum('amt_in_msat')/1000).annotate(amt_out=Sum('amt_out_msat')/1000).annotate(ppm=Round((Sum('fee')*1000000000)/Sum('amt_out_msat'), output_field=IntegerField())).order_by('-id')[:150], + } + return render(request, 'forwards.html', context) + else: + return redirect('home') + + @login_required(login_url='/lndg-admin/login/?next=/') def keysends(request): if request.method == 'GET': @@ -447,11 +616,11 @@ def rebalance(request): 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(): + 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 = [] for channel in form.cleaned_data['outgoing_chan_ids']: chan_ids.append(channel.chan_id) - target_alias = Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey'])[0].alias if Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey']).exists() else None + target_alias = Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey'])[0].alias if Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey']).exists() else '' Rebalancer(value=form.cleaned_data['value'], fee_limit=form.cleaned_data['fee_limit'], outgoing_chan_ids=chan_ids, last_hop_pubkey=form.cleaned_data['last_hop_pubkey'], target_alias=target_alias, duration=form.cleaned_data['duration']).save() messages.success(request, 'Rebalancer request created!') else: @@ -494,7 +663,6 @@ def update_chan_policy(request): messages.error(request, 'Error updating channel policies! Error: ' + error_msg) else: messages.error(request, 'Invalid Request. Please try again.') - print(form.errors) return redirect('home') @login_required(login_url='/lndg-admin/login/?next=/') @@ -521,7 +689,8 @@ def auto_rebalance(request): db_percent_target = LocalSettings.objects.get(key='AR-Target%') db_percent_target.value = target_percent db_percent_target.save() - messages.success(request, 'Updated auto rebalancer rebalance target percent setting to: ' + str(target_percent)) + Channels.objects.all().update(ar_amt_target=Round(F('capacity')*target_percent, output_field=IntegerField())) + messages.success(request, 'Updated auto rebalancer target amount for all channels to: ' + str(target_percent)) if form.cleaned_data['target_time'] is not None: target_time = form.cleaned_data['target_time'] try: @@ -551,7 +720,8 @@ def auto_rebalance(request): db_outbound_target = LocalSettings.objects.get(key='AR-Outbound%') db_outbound_target.value = outbound_percent db_outbound_target.save() - messages.success(request, 'Updated auto rebalancer target outbound percent setting to: ' + str(outbound_percent)) + Channels.objects.all().update(ar_out_target=(round(outbound_percent*100, 0))) + messages.success(request, 'Updated auto rebalancer target outbound percent setting for all channels to: ' + str(outbound_percent)) if form.cleaned_data['fee_rate'] is not None: fee_rate = form.cleaned_data['fee_rate'] try: @@ -572,24 +742,173 @@ def auto_rebalance(request): db_max_cost.value = max_cost db_max_cost.save() messages.success(request, 'Updated auto rebalancer max cost setting to: ' + str(max_cost)) + if form.cleaned_data['autopilot'] is not None: + autopilot = form.cleaned_data['autopilot'] + try: + db_autopilot = LocalSettings.objects.get(key='AR-Autopilot') + except: + LocalSettings(key='AR-Autopilot', value='0').save() + db_autopilot = LocalSettings.objects.get(key='AR-Autopilot') + db_autopilot.value = autopilot + db_autopilot.save() + messages.success(request, 'Updated autopilot setting to: ' + str(autopilot)) else: messages.error(request, 'Invalid Request. Please try again.') - return redirect('home') + return redirect(request.META.get('HTTP_REFERER')) @login_required(login_url='/lndg-admin/login/?next=/') -def ar_target(request): +def update_channel(request): if request.method == 'POST': - form = ARTarget(request.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['ar_target'] + target = form.cleaned_data['target'] + update_target = int(form.cleaned_data['update_target']) db_channel = Channels.objects.filter(chan_id=chan_id)[0] - db_channel.ar_target = target - db_channel.save() - messages.success(request, 'Auto rebalancer inbound target for channel ' + str(chan_id) + ' updated to a value of: ' + str(target) + '%') + if update_target == 0: + stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + channel_point = ln.ChannelPoint() + channel_point.funding_txid_bytes = bytes.fromhex(db_channel.funding_txid) + channel_point.funding_txid_str = db_channel.funding_txid + channel_point.output_index = db_channel.output_index + stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=target, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=40)) + 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(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + channel_point = ln.ChannelPoint() + channel_point.funding_txid_bytes = bytes.fromhex(db_channel.funding_txid) + channel_point.funding_txid_str = db_channel.funding_txid + channel_point.output_index = db_channel.output_index + stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(target/1000000), time_lock_delta=40)) + db_channel.local_fee_rate = target + db_channel.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 == 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 chanel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(db_channel.auto_rebalance)) else: messages.error(request, 'Invalid Request. Please try again.') - return redirect('home') + return redirect(request.META.get('HTTP_REFERER')) + +@login_required(login_url='/lndg-admin/login/?next=/') +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 == 'AR-Target%': + target_percent = value + try: + db_percent_target = LocalSettings.objects.get(key='AR-Target%') + except: + LocalSettings(key='AR-Target%', value='0.05').save() + db_percent_target = LocalSettings.objects.get(key='AR-Target%') + db_percent_target.value = target_percent + db_percent_target.save() + Channels.objects.all().update(ar_amt_target=Round(F('capacity')*target_percent, output_field=IntegerField())) + messages.success(request, 'Updated auto rebalancer target amount for all channels to: ' + str(target_percent)) + elif key == 'AR-Time': + target_time = int(value) + try: + db_time_target = LocalSettings.objects.get(key='AR-Time') + except: + LocalSettings(key='AR-Time', value='5').save() + db_time_target = LocalSettings.objects.get(key='AR-Time') + db_time_target.value = target_time + db_time_target.save() + messages.success(request, 'Updated auto rebalancer target time setting to: ' + str(target_time)) + elif key == 'AR-Enabled': + enabled = int(value) + try: + db_enabled = LocalSettings.objects.get(key='AR-Enabled') + except: + LocalSettings(key='AR-Enabled', value='0').save() + db_enabled = LocalSettings.objects.get(key='AR-Enabled') + db_enabled.value = enabled + db_enabled.save() + messages.success(request, 'Updated auto rebalancer enabled setting to: ' + str(enabled)) + elif key == 'AR-Outbound%': + outbound_percent = float(value) + try: + db_outbound_target = LocalSettings.objects.get(key='AR-Outbound%') + except: + LocalSettings(key='AR-Outbound%', value='0.75').save() + db_outbound_target = LocalSettings.objects.get(key='AR-Outbound%') + db_outbound_target.value = outbound_percent + db_outbound_target.save() + Channels.objects.all().update(ar_out_target=(round(outbound_percent*100, 0))) + messages.success(request, 'Updated auto rebalancer target outbound percent setting for all channels to: ' + str(outbound_percent)) + elif key == 'AR-MaxFeeRate': + fee_rate = int(value) + try: + db_fee_rate = LocalSettings.objects.get(key='AR-MaxFeeRate') + except: + LocalSettings(key='AR-MaxFeeRate', value='100').save() + db_fee_rate = LocalSettings.objects.get(key='AR-MaxFeeRate') + db_fee_rate.value = fee_rate + db_fee_rate.save() + messages.success(request, 'Updated auto rebalancer max fee rate setting to: ' + str(fee_rate)) + elif key == 'AR-MaxCost%': + max_cost = float(value) + try: + db_max_cost = LocalSettings.objects.get(key='AR-MaxCost%') + except: + LocalSettings(key='AR-MaxCost%', value='0.50').save() + db_max_cost = LocalSettings.objects.get(key='AR-MaxCost%') + db_max_cost.value = max_cost + db_max_cost.save() + messages.success(request, 'Updated auto rebalancer max cost setting to: ' + str(max_cost)) + elif key == 'AR-Autopilot': + autopilot = int(value) + try: + db_autopilot = LocalSettings.objects.get(key='AR-Autopilot') + except: + LocalSettings(key='AR-Autopilot', value='0').save() + db_autopilot = LocalSettings.objects.get(key='AR-Autopilot') + db_autopilot.value = autopilot + db_autopilot.save() + messages.success(request, 'Updated autopilot setting to: ' + str(autopilot)) + elif key == 'GUI-GraphLinks': + links = str(value) + try: + db_links = LocalSettings.objects.get(key='GUI-GraphLinks') + except: + LocalSettings(key='GUI-GraphLinks', value='0').save() + db_links = LocalSettings.objects.get(key='GUI-GraphLinks') + db_links.value = links + db_links.save() + messages.success(request, 'Updated graph links to use: ' + str(links)) + elif key == 'GUI-NetLinks': + links = str(value) + try: + db_links = LocalSettings.objects.get(key='GUI-NetLinks') + except: + LocalSettings(key='GUI-NetLinks', value='0').save() + db_links = LocalSettings.objects.get(key='GUI-NetLinks') + db_links.value = links + db_links.save() + messages.success(request, 'Updated network links to use: ' + str(links)) + else: + messages.error(request, 'Invalid Request. Please try again.') + else: + messages.error(request, 'Invalid Request. Please try again.') + return redirect(request.META.get('HTTP_REFERER')) class PaymentsViewSet(viewsets.ReadOnlyModelViewSet): queryset = Payments.objects.all() @@ -659,7 +978,6 @@ def create(self, request): serializer.save() return Response(serializer.data) else: - print(serializer.errors) return Response(serializer.errors) @api_view(['POST']) diff --git a/rebalancer.py b/rebalancer.py index 26515dc3..99ec7082 100644 --- a/rebalancer.py +++ b/rebalancer.py @@ -1,5 +1,5 @@ import django, json, datetime -from django.db.models import Sum +from django.db.models import Sum, F from datetime import datetime, timedelta from gui.lnd_deps import lightning_pb2 as ln from gui.lnd_deps import lightning_pb2_grpc as lnrpc @@ -74,21 +74,15 @@ def auto_schedule(): LocalSettings(key='AR-Enabled', value='0').save() enabled = 0 if enabled == 1: - auto_rebalance_channels = Channels.objects.filter(is_active=True, is_open=True).annotate(percent_outbound=(Sum('local_balance')*100)/Sum('capacity')).annotate(inbound_can=((Sum('remote_balance')*100)/Sum('capacity'))/Sum('ar_target')) + auto_rebalance_channels = Channels.objects.filter(is_active=True, is_open=True).annotate(percent_outbound=(Sum('local_balance')*100)/Sum('capacity')).annotate(inbound_can=((Sum('remote_balance')*100)/Sum('capacity'))/Sum('ar_in_target')) if len(auto_rebalance_channels) > 0: - if LocalSettings.objects.filter(key='AR-Outbound%').exists(): - outbound_percent = int(float(LocalSettings.objects.filter(key='AR-Outbound%')[0].value) * 100) - else: + if not LocalSettings.objects.filter(key='AR-Outbound%').exists(): LocalSettings(key='AR-Outbound%', value='0.75').save() - outbound_percent = 0.75 * 100 - outbound_cans = list(auto_rebalance_channels.filter(auto_rebalance=False, percent_outbound__gte=outbound_percent).values_list('chan_id', flat=True)) + outbound_cans = list(auto_rebalance_channels.filter(auto_rebalance=False, percent_outbound__gte=F('ar_out_target')).values_list('chan_id', flat=True)) inbound_cans = auto_rebalance_channels.filter(auto_rebalance=True, inbound_can__gte=1) if len(inbound_cans) > 0 and len(outbound_cans) > 0: - if LocalSettings.objects.filter(key='AR-Target%').exists(): - target_percent = float(LocalSettings.objects.filter(key='AR-Target%')[0].value) - else: + if not LocalSettings.objects.filter(key='AR-Target%').exists(): LocalSettings(key='AR-Target%', value='0.05').save() - target_percent = 0.05 if LocalSettings.objects.filter(key='AR-MaxFeeRate').exists(): max_fee_rate = int(LocalSettings.objects.filter(key='AR-MaxFeeRate')[0].value) else: @@ -104,7 +98,7 @@ def auto_schedule(): target_fee_rate = int(target.local_fee_rate * max_cost) 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 + target_value = int(target.ar_amt_target / value_per_fee) * value_per_fee if target_value >= value_per_fee: if LocalSettings.objects.filter(key='AR-Time').exists(): target_time = int(LocalSettings.objects.filter(key='AR-Time')[0].value) @@ -121,8 +115,7 @@ def auto_schedule(): print('Creating Auto Rebalance Request') print('Request for:', target.chan_id) print('Request routing through:', outbound_cans) - print('Target % Of Value:', target_percent) - print('Target Value:', target_value) + print('Target Value:', target.ar_amt_target) print('Target Fee:', target_fee) 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() @@ -145,21 +138,24 @@ def auto_enable(): 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') + #print('Case 1: Pass') + 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%') + #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%') + #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') + #print('Case 4: Pass') + pass else: - print('Case 5: Pass') + #print('Case 5: Pass') + pass def main(): rebalances = Rebalancer.objects.filter(status=0).order_by('id') diff --git a/requirements.txt b/requirements.txt index f22fc5ee..d8d7fb48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ djangorestframework django-qr-code grpcio protobuf -pytz \ No newline at end of file +pytz +pandas \ No newline at end of file diff --git a/systemd.md b/systemd.md index 733e1470..e367b2b7 100644 --- a/systemd.md +++ b/systemd.md @@ -93,6 +93,8 @@ User= Group= ExecStart=/usr/bin/bash /home//lndg/htlc_stream.sh StandardError=append:/var/log/lnd_htlc_stream_error.log +Restart=on-failure +RestartSec=60s ``` Enable and start the service to run the htlc failure stream service file. `sudo systemctl enable htlc-stream-lndg.service`