diff --git a/delete_payments.py b/delete_payments.py index 22c039d6..c69666b7 100644 --- a/delete_payments.py +++ b/delete_payments.py @@ -1,10 +1,9 @@ -from lndg import settings from gui.lnd_deps import lightning_pb2 as ln from gui.lnd_deps import lightning_pb2_grpc as lnrpc from gui.lnd_deps.lnd_connect import lnd_connect def main(): - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) try: stub.DeleteAllPayments(ln.DeleteAllPaymentsRequest(failed_payments_only=False, failed_htlcs_only=True)) stub.DeleteAllPayments(ln.DeleteAllPaymentsRequest(failed_payments_only=True, failed_htlcs_only=False)) diff --git a/gui/forms.py b/gui/forms.py index 4b188420..2460e0c6 100644 --- a/gui/forms.py +++ b/gui/forms.py @@ -99,7 +99,8 @@ class AutoRebalanceForm(forms.Form): (7, 'channel_state'), (8, 'auto_fees'), (9, 'cltv'), - (10, 'closing_costs'), + (10, 'min_htlc'), + (11, 'max_htlc'), ] class UpdateChannel(forms.Form): @@ -107,6 +108,21 @@ class UpdateChannel(forms.Form): target = forms.IntegerField(label='target') update_target = forms.ChoiceField(label='update_target', choices=updates_channel_codes) +class UpdateClosing(forms.Form): + funding_txid = forms.CharField(label='funding_txid', max_length=64) + funding_index = forms.IntegerField(label='funding_index') + target = forms.IntegerField(label='target') + +class UpdateKeysend(forms.Form): + r_hash = forms.CharField(label='r_hash', max_length=64) + +class AddAvoid(forms.Form): + pubkey = forms.CharField(label='avoid_pubkey', max_length=66) + notes = forms.CharField(label='avoid_notes', max_length=1000, required=False) + +class RemoveAvoid(forms.Form): + pubkey = forms.CharField(label='avoid_pubkey', max_length=66) + class UpdatePending(forms.Form): funding_txid = forms.CharField(label='funding_txid', max_length=64) output_index = forms.IntegerField(label='output_index') diff --git a/gui/lnd_deps/lnd_connect.py b/gui/lnd_deps/lnd_connect.py index 45b6c064..6593a89d 100644 --- a/gui/lnd_deps/lnd_connect.py +++ b/gui/lnd_deps/lnd_connect.py @@ -1,18 +1,19 @@ import os, codecs, grpc +from lndg import settings -def lnd_connect(LND_DIR_PATH, LND_NETWORK, LND_RPC_SERVER): +def lnd_connect(): #Open connection with lnd via grpc - with open(os.path.expanduser(LND_DIR_PATH + '/data/chain/bitcoin/' + LND_NETWORK + '/admin.macaroon'), 'rb') as f: + with open(os.path.expanduser(settings.LND_MACAROON_PATH), 'rb') as f: macaroon_bytes = f.read() macaroon = codecs.encode(macaroon_bytes, 'hex') def metadata_callback(context, callback): callback([('macaroon', macaroon)], None) os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA' - cert = open(os.path.expanduser(LND_DIR_PATH + '/tls.cert'), 'rb').read() + cert = open(os.path.expanduser(settings.LND_TLS_PATH), 'rb').read() cert_creds = grpc.ssl_channel_credentials(cert) auth_creds = grpc.metadata_call_credentials(metadata_callback) creds = grpc.composite_channel_credentials(cert_creds, auth_creds) - channel = grpc.secure_channel(LND_RPC_SERVER, creds, options=[('grpc.max_send_message_length', 29999999), ('grpc.max_receive_message_length', 29999999),]) + channel = grpc.secure_channel(settings.LND_RPC_SERVER, creds, options=[('grpc.max_send_message_length', 29999999), ('grpc.max_receive_message_length', 29999999),]) return channel def main(): diff --git a/gui/migrations/0021_auto_20220221_1309.py b/gui/migrations/0021_auto_20220221_1309.py index 7395eeff..4d385392 100644 --- a/gui/migrations/0021_auto_20220221_1309.py +++ b/gui/migrations/0021_auto_20220221_1309.py @@ -7,15 +7,14 @@ from gui.lnd_deps import signer_pb2 as lns from gui.lnd_deps import signer_pb2_grpc as lnsigner from gui.lnd_deps.lnd_connect import lnd_connect -from lndg import settings def update_messages(apps, schedma_editor): invoices = apps.get_model('gui', 'invoices') try: messages = invoices.objects.exclude(message=None) if len(messages) > 0: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - signerstub = lnsigner.SignerStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) + signerstub = lnsigner.SignerStub(lnd_connect()) self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey for message in messages: records = stub.LookupInvoice(ln.PaymentHash(r_hash=bytes.fromhex(message.r_hash))).htlcs[0].custom_records @@ -46,7 +45,7 @@ def update_rebal_channel(apps, schedma_editor): payments = apps.get_model('gui', 'payments') hops = apps.get_model('gui', 'paymenthops') try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey for payment in payments.objects.filter(status=2).iterator(): last_hop = hops.objects.filter(payment_hash=payment.payment_hash).order_by('-step')[0] if hops.objects.filter(payment_hash=payment.payment_hash).exists() else None diff --git a/gui/migrations/0032_auto_20220913_1035.py b/gui/migrations/0032_auto_20220913_1035.py new file mode 100644 index 00000000..1f8ed3e4 --- /dev/null +++ b/gui/migrations/0032_auto_20220913_1035.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.7 on 2022-09-13 10:35 + +from django.db import migrations, models + +def migrate_close_fees(apps, schedma_editor): + channels = apps.get_model('gui', 'channels') + closures = apps.get_model('gui', 'closures') + close_fees = channels.objects.filter(closing_costs__gt=0) + for close_fee in close_fees: + closure = closures.objects.filter(funding_txid=close_fee.funding_txid, funding_index=close_fee.output_index)[0] if closures.objects.filter(funding_txid=close_fee.funding_txid, funding_index=close_fee.output_index).exists() else None + if closure: + closure.closing_costs = close_fee.closing_costs + closure.save() + +def revert_close_fees(apps, schedma_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0031_pendingchannels'), + ] + + operations = [ + migrations.AddField( + model_name='closures', + name='closing_costs', + field=models.IntegerField(default=0), + ), + migrations.RunPython(migrate_close_fees, revert_close_fees), + migrations.RemoveField( + model_name='channels', + name='closing_costs', + ), + ] diff --git a/gui/migrations/0033_auto_20220926_1658.py b/gui/migrations/0033_auto_20220926_1658.py new file mode 100644 index 00000000..23c74271 --- /dev/null +++ b/gui/migrations/0033_auto_20220926_1658.py @@ -0,0 +1,51 @@ +# Generated by Django 3.2.7 on 2022-09-27 01:38 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0032_auto_20220913_1035'), + ] + + operations = [ + migrations.CreateModel( + name='AvoidNodes', + fields=[ + ('pubkey', models.CharField(max_length=66, primary_key=True, serialize=False)), + ('notes', models.CharField(max_length=1000, null=True)), + ('updated', models.DateTimeField(default=django.utils.timezone.now)), + ], + ), + migrations.AddField( + model_name='channels', + name='local_max_htlc_msat', + field=models.BigIntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='channels', + name='local_min_htlc_msat', + field=models.BigIntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='channels', + name='remote_max_htlc_msat', + field=models.BigIntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='channels', + name='remote_min_htlc_msat', + field=models.BigIntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='invoices', + name='is_revenue', + field=models.BooleanField(default=False), + ), + ] diff --git a/gui/models.py b/gui/models.py index 1011dd7d..7d21019e 100644 --- a/gui/models.py +++ b/gui/models.py @@ -47,6 +47,7 @@ class Invoices(models.Model): sender = models.CharField(null=True, max_length=66) sender_alias = models.CharField(null=True, max_length=32) index = models.IntegerField() + is_revenue = models.BooleanField(default=False) class Meta: app_label = 'gui' @@ -86,10 +87,14 @@ class Channels(models.Model): local_fee_rate = models.IntegerField() local_disabled = models.BooleanField() local_cltv = models.IntegerField() + local_min_htlc_msat = models.BigIntegerField() + local_max_htlc_msat = models.BigIntegerField() remote_base_fee = models.IntegerField() remote_fee_rate = models.IntegerField() remote_disabled = models.BooleanField() remote_cltv = models.IntegerField() + remote_min_htlc_msat = models.BigIntegerField() + remote_max_htlc_msat = models.BigIntegerField() is_active = models.BooleanField() is_open = models.BooleanField() last_update = models.DateTimeField() @@ -100,7 +105,6 @@ class Channels(models.Model): ar_max_cost = models.IntegerField() fees_updated = models.DateTimeField(default=timezone.now) auto_fees = models.BooleanField() - closing_costs = models.IntegerField(default=0) def save(self, *args, **kwargs): if self.auto_fees is None: @@ -203,6 +207,7 @@ class Closures(models.Model): open_initiator = models.IntegerField() close_initiator = models.IntegerField() resolution_count = models.IntegerField() + closing_costs = models.IntegerField(default=0) class Meta: app_label = 'gui' unique_together = (('funding_txid', 'funding_index'),) @@ -279,4 +284,11 @@ class PendingChannels(models.Model): auto_fees = models.BooleanField(null=True, default=None) class Meta: app_label = 'gui' - unique_together = (('funding_txid', 'output_index'),) \ No newline at end of file + unique_together = (('funding_txid', 'output_index'),) + +class AvoidNodes(models.Model): + pubkey = models.CharField(max_length=66, primary_key=True) + notes = models.CharField(null=True, max_length=1000) + updated = models.DateTimeField(default=timezone.now) + class Meta: + app_label = 'gui' \ No newline at end of file diff --git a/gui/static/favicon.ico b/gui/static/favicon.ico new file mode 100644 index 00000000..ec8ee03d Binary files /dev/null and b/gui/static/favicon.ico differ diff --git a/gui/templates/advanced.html b/gui/templates/advanced.html index 7b9eabe7..dd32b11e 100644 --- a/gui/templates/advanced.html +++ b/gui/templates/advanced.html @@ -30,7 +30,14 @@

Advanced Channel Settings

- + +
+ {% csrf_token %} + + +
+ +
{% csrf_token %} @@ -79,6 +86,8 @@

Advanced Channel Settings

oRate oBase oCLTV + minHTLC + maxHTLC iRate iBase Target Amt @@ -133,6 +142,22 @@

Advanced Channel Settings

+ +
+ {% csrf_token %} + + + +
+ + +
+ {% csrf_token %} + + + +
+ {{ channel.remote_fee_rate|intcomma }} {{ channel.remote_base_fee|intcomma }} @@ -204,7 +229,7 @@

Update Local Settings

{% csrf_token %} {% if settings.key == 'AR-Target%' %} - {% elif settings.key|slice:"-1:" == '%' or settings.key == 'AR-Variance' or settings.key == 'AF-Increment' or settings.key == 'AF-Multiplier' or settings.key == 'AF-FailedHTLCs' or settings.key == 'AR-WaitPeriod' or settings.key == 'AR-APDays' %} + {% elif settings.key|slice:"-1:" == '%' or settings.key == 'AR-Variance' or settings.key == 'AF-Increment' or settings.key == 'AF-Multiplier' or settings.key == 'AF-FailedHTLCs' or settings.key == 'AR-WaitPeriod' or settings.key == 'AR-APDays' or settings.key == 'AF-UpdateHours' %} {% elif settings.key == 'AR-Time' %} diff --git a/gui/templates/autofees.html b/gui/templates/autofees.html index b40d47bc..11baef63 100644 --- a/gui/templates/autofees.html +++ b/gui/templates/autofees.html @@ -4,7 +4,7 @@ {% load humanize %} {% if autofees %}
-

Autofees Logs

+

Our Fees Logs

@@ -18,7 +18,7 @@

Autofees Logs

- + @@ -29,8 +29,8 @@

Autofees Logs

{% endif %} {% if not autofees %}
-

No autofees logs to see here yet!

+

No our fees logs to see here yet!

Experimental. This will allow LNDg to automatically act upon the suggestions found here.
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/autopilot.html b/gui/templates/autopilot.html index e47e8375..f65817ba 100644 --- a/gui/templates/autopilot.html +++ b/gui/templates/autopilot.html @@ -4,7 +4,7 @@ {% load humanize %} {% if autopilot %}
-

Autopilot Logs

+

Autopilot Logs

Timestamp
{{ log.timestamp|naturaltime }} {{ log.chan_id }}{% if log.peer_alias == '' %}---{% else %}{{ log.peer_alias }}{% endif %}{% if log.peer_alias == '' %}---{% else %}{{ log.peer_alias }}{% endif %} {{ log.setting }} {{ log.old_value }} log.old_value %}style="background-color: #d5fadb"{% else %}style="background-color: #fadbd5"{% endif %}>{{ log.new_value }}
@@ -18,7 +18,7 @@

Autopilot Logs

- + @@ -33,4 +33,4 @@

Autopilot Logs

Experimental. This will allow LNDg to automatically act upon the suggestions found here.
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/base.html b/gui/templates/base.html index bf24086b..fdb717de 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -4,6 +4,7 @@ + {% block meta %}{% endblock %} {% block title %}LNDg{% endblock %} {% load static %} {% load qr_code %} @@ -28,7 +29,7 @@

My Lnd Overview

diff --git a/gui/templates/channel.html b/gui/templates/channel.html index 62601208..b8e672e3 100644 --- a/gui/templates/channel.html +++ b/gui/templates/channel.html @@ -6,6 +6,7 @@

Details For Channel: {{ chan_id }} | Peer: {% if channel.alias != "" %}{{ channel.alias }} ({{ channel.remote_pubkey }}){% else %}{{ channel.remote_pubkey }}{% endif %}

Capacity: {{ channel.capacity|intcomma }} | Is Open: {{ channel.is_open }} | Is Active: {{ channel.is_active }} | Is Private: {{ channel.private }} | Channel Updates: {{ channel.num_updates|intcomma }}

+ {% if peer_info %}

Peer Connected: {{ peer_info.connected }} | Peer Address: {{ peer_info.address }} | {% if not channel.is_open %}Marked Closed:{% elif channel.is_active %}Uptime:{% else %}Downtime:{% endif %} {{ channel.last_update|naturaltime|slice:":-4" }} (since {{ channel.last_update }})

{% endif %}

Channel Point: {{ channel.funding_txid }}:{{ channel.output_index }} | Opened In: {{ channel.open_block|intcomma }} | Opener: {% if channel.initiator == True %}Local{% else %}Remote{% endif %}

Outbound Liquidity: {{ channel.local_balance|intcomma }} ({{ channel.out_percent }}%) | Inbound Liquidity: {{ channel.remote_balance|intcomma }} ({{ channel.in_percent }}%)

@@ -166,7 +167,7 @@

Outgoing HTLCs

- + {% endfor %}
Timestamp
{{ log.timestamp|naturaltime }} {{ log.chan_id }}{% if log.peer_alias == '' %}---{% else %}{{ log.peer_alias }}{% endif %}{% if log.peer_alias == '' %}---{% else %}{{ log.peer_alias }}{% endif %} {{ log.setting }} {{ log.old_value }} {{ log.new_value }} {% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %} {{ htlc.amount|intcomma }} {{ htlc.expiration_height|intcomma }}{{ htlc.hash_lock }}{{ htlc.hash_lock }}
@@ -195,6 +196,33 @@

Incoming HTLCs

{% endif %} +{% if autofees %} +
+

30 Days Our Fees Logs

+ + + + + + + + + + + {% for log in autofees %} + + + + + + + + + + {% endfor %} +
TimestampChannel IDPeer AliasSettingOld ValueNew ValueChange
{{ log.timestamp|naturaltime }}{{ log.chan_id }}{% if log.peer_alias == '' %}---{% else %}{{ log.peer_alias }}{% endif %}{{ log.setting }}{{ log.old_value }} log.old_value %}style="background-color: #d5fadb"{% else %}style="background-color: #fadbd5"{% endif %}>{{ log.new_value }} log.old_value %}style="background-color: #d5fadb"{% else %}style="background-color: #fadbd5"{% endif %}>{{ log.change }}%
+
+{% endif %} {% if forwards %}

Last 5 Payments Routed

@@ -242,6 +270,7 @@

Last 5 Rebalance Requests

Fees Paid Last Hop Alias Status + Hash {% for rebalance in rebalances %} @@ -254,8 +283,9 @@

Last 5 Rebalance Requests

{{ rebalance.fee_limit|intcomma }} {{ rebalance.ppm|intcomma }} {% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %} - {% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %} + {% if rebalance.target_alias == '' %}---{% 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.payment_hash == '' %}---{% else %}{{ rebalance.payment_hash }}{% endif %} {% endfor %} @@ -280,7 +310,7 @@

Last 5 Payments Sent

{% for payment in payments %} {{ payment.creation_date|naturaltime }} - {{ payment.payment_hash }} + {{ payment.payment_hash }} {{ payment.value|add:"0"|intcomma }} {{ payment.fee|intcomma }} {{ payment.ppm|intcomma }} @@ -313,7 +343,7 @@

Last 5 Payments Received

{{ invoice.creation_date|naturaltime }} {% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}---{% endif %} - {{ invoice.r_hash }} + {{ invoice.r_hash }} {{ invoice.value|add:"0"|intcomma }} {% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}---{% endif %} {% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %} @@ -346,13 +376,13 @@

Last 5 Failed HTLCs

{{ failed_htlc.timestamp|naturaltime }} {{ failed_htlc.chan_id_in }} {{ failed_htlc.chan_id_out }} - {% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %} - {% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %} + {% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %} + {% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %} {{ failed_htlc.amount|intcomma }} {{ failed_htlc.chan_out_liq|intcomma }} ({{ failed_htlc.chan_out_pending|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 %} + {% 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 == 13 %}Invoice Not Open{% elif failed_htlc.failure_detail == 20 %}Invalid Keysend{% elif failed_htlc.failure_detail == 22 %}Circular Route{% else %}{{ failed_htlc.failure_detail }}{% endif %} {% endfor %} diff --git a/gui/templates/channels.html b/gui/templates/channels.html index f1242bff..6a8bcb85 100644 --- a/gui/templates/channels.html +++ b/gui/templates/channels.html @@ -34,7 +34,7 @@

Channel Performance

{{ channel.chan_id }} {% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} - {{ channel.mil_capacity }} M + {{ channel.mil_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.apy_7day }}% | {{ channel.cv_7day }}% {{ channel.revenue_7day|intcomma }} [{{ channel.profits_7day|intcomma }}] | {{ channel.revenue_assist_7day|intcomma }} diff --git a/gui/templates/closures.html b/gui/templates/closures.html index dce4c67c..e6f90656 100644 --- a/gui/templates/closures.html +++ b/gui/templates/closures.html @@ -117,7 +117,7 @@

Closures

Opener Closer Resolutions - Costs + Costs 🗘 {% for closure in closures %} @@ -133,16 +133,16 @@

Closures

{% if closure.close_initiator == 0 %}Unknown{% elif closure.close_initiator == 1 %}Local{% elif closure.close_initiator == 2 %}Remote{% elif closure.close_initiator == 3 %}Both{% else %}{{ closure.close_initiator }}{% endif %} {% if closure.resolution_count > 0 %}Details{% else %}---{% endif %} - {% if closure.closing_costs == '' %} + {% if closure.close_type == 4 or closure.close_type == 5 %} --- - {% elif closure.open_initiator == 2 and closure.close_type == 0 %} + {% elif closure.open_initiator == 2 and closure.resolution_count == 0 %} --- {% else %} -
+ {% csrf_token %} - - + +
{% endif %} diff --git a/gui/templates/failed_htlcs.html b/gui/templates/failed_htlcs.html index 8288ab3f..cb55420a 100644 --- a/gui/templates/failed_htlcs.html +++ b/gui/templates/failed_htlcs.html @@ -4,7 +4,7 @@ {% load humanize %} {% if failed_htlcs %}
-

Last 150 Failed HTLCs

+

Last 150 Failed HTLCs

@@ -23,12 +23,12 @@

Last 150 Failed HTLCs

- - + + - + {% endfor %} diff --git a/gui/templates/fee_rates.html b/gui/templates/fee_rates.html index 12eb24db..f66d7dc4 100644 --- a/gui/templates/fee_rates.html +++ b/gui/templates/fee_rates.html @@ -18,7 +18,7 @@

Auto-Fees Settings

{% csrf_token %} {% if settings.key == 'AF-MaxRate' or settings.key == 'AF-MinRate' %} - {% elif settings.key == 'AF-Increment' or settings.key == 'AF-Multiplier' or settings.key == 'AF-FailedHTLCs' or settings.key == 'AR-WaitPeriod' %} + {% elif settings.key == 'AF-Increment' or settings.key == 'AF-Multiplier' or settings.key == 'AF-FailedHTLCs' or settings.key == 'AF-UpdateHours' %} {% else %} diff --git a/gui/templates/home.html b/gui/templates/home.html index 0c0ebcd6..d8a1c57b 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -1,10 +1,16 @@ {% extends "base.html" %} {% block title %} {{ block.super }} - Dashboard{% endblock %} +{% block meta %}{% endblock %} {% block content %} {% load humanize %}
+

Dashboard Last Updated : ... refreshing every 21 minutes

+

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

-

Public Capacity: {{ total_capacity|intcomma }} | Active Channels: {{ node_info.num_active_channels }} / {{ total_channels }} | Peers: {{ node_info.num_peers }} | DB Size: {% if db_size > 0 %}{{ db_size }} GB{% else %}---{% endif %} | Total States Updates: {% if num_updates > 0 %}{{ num_updates|intcomma }} {% else %}---{% endif %}

+

Public Capacity: {{ total_capacity|intcomma }} | Active Channels: {{ active_count }} / {{ total_channels }} | Peers: {{ node_info.num_peers }} | DB Size: {% if db_size > 0 %}{{ db_size }} GB{% else %}---{% endif %} | Total States Updates: {% if num_updates > 0 %}{{ num_updates|intcomma }} {% else %}---{% endif %}

{% if total_private > 0 %}

Private Capacity: {{ private_capacity|intcomma }} | Locked Liquidity: {{ private_outbound|intcomma }} | Active Private Channels: {{ active_private }} / {{ total_private }}

{% endif %}

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

@@ -453,20 +459,22 @@

Last 10 Rebalance Requests (curren

+ {% for rebalance in rebalances %} - - + + - - + + + {% endfor %}
Timestamp {{ failed_htlc.timestamp|naturaltime }} {{ failed_htlc.chan_id_in }} {{ failed_htlc.chan_id_out }}{% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %}{% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %}{% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %}{% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %} {{ failed_htlc.amount|intcomma }} {{ failed_htlc.chan_out_liq|intcomma }} ({{ failed_htlc.chan_out_pending|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.wire_failure == 15 %}Temporary Channel Failure{% elif failed_htlc.wire_failure == 18 %}Unknown Next Peer{% elif failed_htlc.wire_failure == 12 %}Fee Insufficient{% elif failed_htlc.wire_failure == 22 %}Expiry Too Far{% 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 == 13 %}Invoice Not Open{% elif failed_htlc.failure_detail == 20 %}Invalid Keysend{% elif failed_htlc.failure_detail == 22 %}Circular Route{% else %}{{ failed_htlc.failure_detail }}{% endif %}
Fees Paid Last Hop Alias StatusHash
{{ rebalance.requested|naturaltime }}{% if rebalance.status == 0 %}---{% else %}{{ rebalance.start|naturaltime }}{% endif %}{% if rebalance.status > 1 %}{{ rebalance.stop|naturaltime }}{% else %}---{% endif %}---{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %} 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>---{% endif %} {{ rebalance.duration }} minutes {% if rebalance.status == 2 %}{{ rebalance.stop|timeuntil:rebalance.start }}{% else %}---{% endif %} {{ rebalance.value|intcomma }} {{ rebalance.fee_limit|intcomma }} {{ rebalance.ppm|intcomma }}{% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma}}{% else %}---{% endif %}{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}{% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %}{% if rebalance.target_alias == '' %}---{% 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.payment_hash == '' %}---{% else %}{{ rebalance.payment_hash }}{% endif %}
@@ -491,7 +499,7 @@

Last 5 Payments Sent

{% for payment in payments %} {{ payment.creation_date|naturaltime }} - {{ payment.payment_hash }} + {{ payment.payment_hash }} {{ payment.value|add:"0"|intcomma }} {{ payment.fee|intcomma }} {{ payment.ppm|intcomma }} @@ -524,7 +532,7 @@

Last 5 Payments Received

{{ invoice.creation_date|naturaltime }} {% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}---{% endif %} - {{ invoice.r_hash }} + {{ invoice.r_hash }} {{ invoice.value|add:"0"|intcomma }} {% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}---{% endif %} {% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %} @@ -557,8 +565,8 @@

Last 10 Failed HTLCs

{{ failed_htlc.timestamp|naturaltime }} {{ failed_htlc.chan_id_in }} {{ failed_htlc.chan_id_out }} - {% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %} - {% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %} + {% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %} + {% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %} {{ failed_htlc.amount|intcomma }} {{ failed_htlc.chan_out_liq|intcomma }} ({{ failed_htlc.chan_out_pending|intcomma }}) {{ failed_htlc.missed_fee|intcomma }} diff --git a/gui/templates/invoices.html b/gui/templates/invoices.html index fa799aa2..08109181 100644 --- a/gui/templates/invoices.html +++ b/gui/templates/invoices.html @@ -21,7 +21,7 @@

Last 150 Invoices

{{ invoice.creation_date|naturaltime }} {% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}---{% endif %} - {{ invoice.r_hash }} + {{ invoice.r_hash }} {{ invoice.value|add:"0"|intcomma }} {% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}---{% endif %} {% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %} diff --git a/gui/templates/keysends.html b/gui/templates/keysends.html index 9f229362..ffd7ef10 100644 --- a/gui/templates/keysends.html +++ b/gui/templates/keysends.html @@ -7,6 +7,7 @@

Received Keysends

+ @@ -14,9 +15,16 @@

Received Keysends

{% for keysend in keysends %} - - - + + + + {% endfor %} diff --git a/gui/templates/open_list.html b/gui/templates/open_list.html index e023d32b..7960415c 100644 --- a/gui/templates/open_list.html +++ b/gui/templates/open_list.html @@ -32,10 +32,53 @@

Suggested Open List

Revenue? Settle Date Channel In Alias Amount
{{ keysend.settle_date|naturaltime }}{% if keysend.chan_in_alias == '' %}---{% else %}{{ keysend.chan_in_alias }}{% endif %}{{ keysend.amt_paid|intcomma }} +
+ {% csrf_token %} + + +
+
{{ keysend.settle_date|naturaltime }}{% if keysend.chan_in_alias == '' %}---{% else %}{{ keysend.chan_in_alias }}{% endif %}{{ keysend.amt_paid|intcomma }} {{ keysend.message }}{% if keysend.sender != None %} | Signed By: {% if keysend.sender_alias != None %}{{ keysend.sender_alias }}{% else %}{{ keysend.sender }}{% endif %}{% endif %}
-{% endif %} -{% if not open_list %} +{% else %}

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

{% endif %} +{% if avoid_list %} +
+

Avoid/Exclude List

+
+ + + + + + + + {% for node in avoid_list %} + + + + + + + {% endfor %} +
UpdatedNode PubkeyNotesRemove
{{ node.updated|naturaltime }}{{ node.pubkey }}{% if node.notes == '' %}---{% else %}{{ node.notes }}{% endif %} +
+ {% csrf_token %} + + +
+
+
+
+{% else %} +
+

No node added to the exclusion list yet. Add nodes here you want to avoid connecting to in the future.

+
+{% endif %} +
+

Add Node To Exclusion List Or Update Existing Notes

+
+ {% csrf_token %} + + + + + +
+
{% endblock %} diff --git a/gui/templates/payments.html b/gui/templates/payments.html index bf7ad3de..42f0f244 100644 --- a/gui/templates/payments.html +++ b/gui/templates/payments.html @@ -21,7 +21,7 @@

Last 150 Payments

{% for payment in payments %} {{ payment.creation_date|naturaltime }} - {{ payment.payment_hash }} +
{{ payment.payment_hash }} {{ payment.value|add:"0"|intcomma }} {{ payment.fee|intcomma }} {{ payment.ppm|intcomma }} diff --git a/gui/templates/pending_htlcs.html b/gui/templates/pending_htlcs.html index babce0e5..b32b572a 100644 --- a/gui/templates/pending_htlcs.html +++ b/gui/templates/pending_htlcs.html @@ -23,7 +23,7 @@

Outgoing HTLCs

{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %} {{ htlc.amount|intcomma }} {{ htlc.hours_til_expiration }} hours - {{ htlc.hash_lock }} +
{{ htlc.hash_lock }} {% endfor %} @@ -50,7 +50,7 @@

Incoming HTLCs

{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %} {{ htlc.amount|intcomma }} {{ htlc.hours_til_expiration }} hours - {{ htlc.hash_lock }} + {{ htlc.hash_lock }} {% endfor %} diff --git a/gui/templates/rebalances.html b/gui/templates/rebalances.html index 2e23f7e2..b2f3dda2 100644 --- a/gui/templates/rebalances.html +++ b/gui/templates/rebalances.html @@ -2,6 +2,43 @@ {% block title %} {{ block.super }} - Rebalances{% endblock %} {% block content %} {% load humanize %} +{% if rebalances_success %} +
+

Last Successful Rebalances

+ + + + + + + + + + + + + + + + {% for rebalance in rebalances_success %} + + + + + + + + + + + + + + + {% endfor %} +
RequestedStartStopScheduled DurationActual DurationValueFee LimitTarget PPMFees PaidLast Hop AliasStatusHash
{{ rebalance.requested|naturaltime }}---{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %} 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>---{% endif %}{{ rebalance.duration }} minutes{% if rebalance.status == 2 %}{{ rebalance.stop|timeuntil:rebalance.start }}{% else %}---{% endif %}{{ rebalance.value|intcomma }}{{ rebalance.fee_limit|intcomma }}{{ rebalance.ppm|intcomma }}{% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %}{% if rebalance.target_alias == '' %}---{% 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.payment_hash == '' %}---{% else %}{{ rebalance.payment_hash }}{% endif %}
+
+{% endif %} {% if rebalances %}

Last 150 Rebalances

@@ -18,6 +55,7 @@

Last 150 Rebalances

Fees Paid Last Hop Alias Status + Hash {% for rebalance in rebalances %} @@ -30,8 +68,9 @@

Last 150 Rebalances

{{ rebalance.fee_limit|intcomma }} {{ rebalance.ppm|intcomma }} {% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %} - {% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %} + {% if rebalance.target_alias == '' %}---{% 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.payment_hash == '' %}---{% else %}{{ rebalance.payment_hash }}{% endif %} {% endfor %} diff --git a/gui/templates/rebalancing.html b/gui/templates/rebalancing.html index 284d605b..bb626f26 100644 --- a/gui/templates/rebalancing.html +++ b/gui/templates/rebalancing.html @@ -4,7 +4,7 @@ {% load humanize %} {% if channels %}
-

Channel Rebalancing (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing via {{ available_count }} outbound channels)

+

Channel Rebalancing (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing via {{ available_count }} outbound channels)

@@ -14,14 +14,14 @@

Channel Rebalancing (currently scheduling {{ eligible_count }} of {{ enabled

- - + + - + @@ -108,6 +108,7 @@

Last 20 Rebalance Requests

+ {% for rebalance in rebalancer %} @@ -120,8 +121,9 @@

Last 20 Rebalance Requests

- + + {% endfor %}
Outbound Liquidity Inbound LiquidityRebal Out?Enabled?Rebal Out?Enabled? Fee Ratio Rebal In? Target Amt Max Cost % oTarget%iTarget%iTarget% AR 7-Day Rate Active Fees Paid Last Hop Alias StatusHash
{{ rebalance.fee_limit|intcomma }} {{ rebalance.ppm|intcomma }} {% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %}{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}{% if rebalance.target_alias == '' %}---{% 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.payment_hash == '' %}---{% else %}{{ rebalance.payment_hash }}{% endif %}
diff --git a/gui/templates/route.html b/gui/templates/route.html index ddefd138..5094f1f5 100644 --- a/gui/templates/route.html +++ b/gui/templates/route.html @@ -4,9 +4,10 @@ {% load humanize %} {% if route %}
-

Route For : {{ payment_hash }}

+

Route For : {{ payment_hash }}{% if total_cost %} | Total Costs: {{ total_cost }} [{{ total_ppm }}]{% endif %}

+ @@ -19,6 +20,7 @@

Route For : {{ payment_hash }}

{% for hop in route %} + @@ -38,4 +40,124 @@

Route For : {{ payment_hash }}

A route was not found for this payment hash!

{% endif %} +{% if rebalances %} +
+

Associated Rebalances

+
Attempt Id Step Amount Fee
{{ hop.attempt_id }} {{ hop.step }} {{ hop.amt|intcomma }} {{ hop.fee|intcomma }}
+ + + + + + + + + + + + + + {% for rebalance in rebalances %} + + + + + + + + + + + + + + {% endfor %} +
RequestedStartStopScheduled DurationActual DurationValueFee LimitTarget PPMFees PaidLast Hop AliasStatus
{{ rebalance.requested|naturaltime }}---{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %} 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>---{% endif %}{{ rebalance.duration }} minutes{% if rebalance.status == 2 %}{{ rebalance.stop|timeuntil:rebalance.start }}{% else %}---{% endif %}{{ rebalance.value|intcomma }}{{ rebalance.fee_limit|intcomma }}{{ rebalance.ppm|intcomma }}{% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %}{% 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 %}
+
+{% endif %} +{% if invoices %} +
+

Linked Invoice

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

Outgoing HTLCs

+ + + + + + + + + + + {% for htlc in outgoing_htlcs %} + + + + + + + + + + {% endfor %} +
Channel IDChannel AliasForwarding ChannelForwarding AliasAmountExpirationHash Lock
{{ htlc.chan_id }}{% if htlc.alias == '' %}---{% else %}{{ htlc.alias }}{% endif %}{% if htlc.forwarding_channel == 0 %}---{% else %}{{ htlc.forwarding_channel }}{% endif %}{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %}{{ htlc.amount|intcomma }}{{ htlc.hours_til_expiration }} hours{{ htlc.hash_lock }}
+
+{% endif %} +{% if incoming_htlcs %} +
+

Incoming HTLCs

+ + + + + + + + + + + {% for htlc in incoming_htlcs %} + + + + + + + + + + {% endfor %} +
Channel IDChannel AliasForwarding ChannelForwarding AliasAmountExpirationHash Lock
{{ htlc.chan_id }}{% if htlc.alias == '' %}---{% else %}{{ htlc.alias }}{% endif %}{% if htlc.forwarding_channel == 0 %}---{% else %}{{ htlc.forwarding_channel }}{% endif %}{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %}{{ htlc.amount|intcomma }}{{ htlc.hours_til_expiration }} hours{{ htlc.hash_lock }}
+
+{% endif %} {% endblock %} diff --git a/gui/urls.py b/gui/urls.py index 8666d5d6..c163864e 100644 --- a/gui/urls.py +++ b/gui/urls.py @@ -52,6 +52,11 @@ path('update_channel/', views.update_channel, name='update-channel'), path('update_pending/', views.update_pending, name='update-pending'), path('update_setting/', views.update_setting, name='update-setting'), + path('update_closing/', views.update_closing, name='update-closing'), + path('update_keysend/', views.update_keysend, name='update-keysend'), + path('add_avoid/', views.add_avoid, name='add-avoid'), + path('remove_avoid/', views.remove_avoid, name='remove-avoid'), + path('get_fees/', views.get_fees, name='get-fees'), path('opens/', views.opens, name='opens'), path('actions/', views.actions, name='actions'), path('fees/', views.fees, name='fees'), @@ -70,6 +75,7 @@ path('api/updatealias/', views.update_alias, name='update-alias'), path('api/getinfo/', views.get_info, name='get-info'), path('api/balances/', views.api_balances, name='api-balances'), + path('api/income/', views.api_income, name='api-income'), path('api/pendingchannels/', views.pending_channels, name='pending-channels'), path('lndg-admin/', admin.site.urls), ] diff --git a/gui/views.py b/gui/views.py index 68540294..1daa2219 100644 --- a/gui/views.py +++ b/gui/views.py @@ -3,13 +3,13 @@ from django.db.models import Sum, IntegerField, Count, F, Q from django.db.models.functions import Round from django.contrib.auth.decorators import login_required -from django.conf import settings from datetime import datetime, timedelta 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, UpdateChannel, UpdateSetting, AutoRebalanceForm, AddTowerForm, RemoveTowerForm, DeleteTowerForm, BatchOpenForm, UpdatePending -from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, Closures, Resolutions, PendingHTLCs, FailedHTLCs, Autopilot, Autofees, PendingChannels +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from .forms import OpenChannelForm, CloseChannelForm, ConnectPeerForm, AddInvoiceForm, RebalancerForm, ChanPolicyForm, UpdateChannel, UpdateSetting, AutoRebalanceForm, AddTowerForm, RemoveTowerForm, DeleteTowerForm, BatchOpenForm, UpdatePending, UpdateClosing, UpdateKeysend, AddAvoid, RemoveAvoid +from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, Closures, Resolutions, PendingHTLCs, FailedHTLCs, Autopilot, Autofees, PendingChannels, AvoidNodes from .serializers import ConnectPeerSerializer, FailedHTLCSerializer, LocalSettingsSerializer, OpenChannelSerializer, CloseChannelSerializer, AddInvoiceSerializer, PaymentHopsSerializer, PaymentSerializer, InvoiceSerializer, ForwardSerializer, ChannelSerializer, PendingHTLCSerializer, RebalancerSerializer, UpdateAliasSerializer, PeerSerializer, OnchainSerializer, ClosuresSerializer, ResolutionsSerializer from gui.lnd_deps import lightning_pb2 as ln from gui.lnd_deps import lightning_pb2_grpc as lnrpc @@ -17,10 +17,11 @@ from gui.lnd_deps import router_pb2_grpc as lnrouter from gui.lnd_deps import wtclient_pb2 as wtrpc from gui.lnd_deps import wtclient_pb2_grpc as wtstub -from .lnd_deps.lnd_connect import lnd_connect -from lndg.settings import LND_NETWORK, LND_DIR_PATH +from gui.lnd_deps.lnd_connect import lnd_connect +from lndg import settings from os import path from pandas import DataFrame, merge +from requests import get def graph_links(): if LocalSettings.objects.filter(key='GUI-GraphLinks').exists(): @@ -38,11 +39,28 @@ def network_links(): network_links = 'https://mempool.space' return network_links -@login_required(login_url='/lndg-admin/login/?next=/') +def get_tx_fees(txid): + base_url = network_links() + ('/testnet' if settings.LND_NETWORK == 'testnet' else '') + '/api/tx/' + request_data = get(base_url + txid).json() + fee = request_data['fee'] + return fee + +class is_login_required(object): + def __init__(self, dec, condition): + self.decorator = dec + self.condition = condition + + def __call__(self, func): + if not self.condition: + # No login required + return func + return self.decorator(func) + +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def home(request): if request.method == 'GET': try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) #Get balance and general node information node_info = stub.GetInfo(ln.GetInfoRequest()) balances = stub.WalletBalance(ln.WalletBalanceRequest()) @@ -156,6 +174,8 @@ def home(request): pending_outbound = channels.filter(is_open=True).aggregate(Sum('pending_outbound'))['pending_outbound__sum'] if channels.filter(is_open=True).exists() else 0 pending_inbound = channels.filter(is_open=True).aggregate(Sum('pending_inbound'))['pending_inbound__sum'] if channels.filter(is_open=True).exists() else 0 num_updates = channels.filter(is_open=True).aggregate(Sum('num_updates'))['num_updates__sum'] if channels.filter(is_open=True).exists() else 0 + eligible_count = 0 + available_count = 0 detailed_active_channels = [] for channel in active_channels: detailed_channel = {} @@ -189,6 +209,13 @@ def home(request): detailed_channel['htlc_count'] = channel.htlc_count detailed_channel['auto_rebalance'] = channel.auto_rebalance detailed_channel['ar_in_target'] = channel.ar_in_target + detailed_channel['inbound_can'] = (detailed_channel['remote_balance']/channel.capacity)*100 + detailed_channel['outbound_can'] = (detailed_channel['local_balance']/channel.capacity)*100 + detailed_channel['fee_ratio'] = 100 if channel.local_fee_rate == 0 else (channel.remote_fee_rate/channel.local_fee_rate)*100 + if channel.auto_rebalance == True and detailed_channel['inbound_can'] >= channel.ar_in_target and detailed_channel['fee_ratio'] <= channel.ar_max_cost: + eligible_count += 1 + if channel.auto_rebalance == False and detailed_channel['outbound_can'] >= channel.ar_out_target: + available_count += 1 detailed_active_channels.append(detailed_channel) #Get current inactive channels inactive_channels = channels.filter(is_active=False, is_open=True, private=False).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity')).order_by('outbound_percent') @@ -205,8 +232,8 @@ def home(request): onchain_txs = Onchain.objects.all() onchain_costs_7day = 0 if onchain_txs.filter(time_stamp__gte=filter_7day).count() == 0 else onchain_txs.filter(time_stamp__gte=filter_7day).aggregate(Sum('fee'))['fee__sum'] onchain_costs_1day = 0 if onchain_txs.filter(time_stamp__gte=filter_1day).count() == 0 else onchain_txs.filter(time_stamp__gte=filter_1day).aggregate(Sum('fee'))['fee__sum'] - closures_7day = channels.filter(chan_id__in=Closures.objects.filter(close_height__gte=(node_info.block_height - 1008)).values('chan_id')) - closures_1day = channels.filter(chan_id__in=Closures.objects.filter(close_height__gte=(node_info.block_height - 144)).values('chan_id')) + closures_7day = Closures.objects.filter(close_height__gte=(node_info.block_height - 1008)) + closures_1day = Closures.objects.filter(close_height__gte=(node_info.block_height - 144)) close_fees_7day = closures_7day.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_7day.exists() else 0 close_fees_1day = closures_1day.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_1day.exists() else 0 onchain_costs_7day += close_fees_7day @@ -215,15 +242,17 @@ def home(request): total_costs_1day = total_1day_fees + onchain_costs_1day #Get list of recent rebalance requests rebalances = Rebalancer.objects.all().annotate(ppm=Round((Sum('fee_limit')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-id') + active_count = node_info.num_active_channels - active_private total_channels = node_info.num_active_channels + node_info.num_inactive_channels - private_count local_settings = LocalSettings.objects.filter(key__contains='AR-').order_by('key') try: - db_size = round(path.getsize(path.expanduser(LND_DIR_PATH + '/data/graph/' + LND_NETWORK + '/channel.db'))*0.000000001, 3) + db_size = round(path.getsize(path.expanduser(settings.LND_DATABASE_PATH))*0.000000001, 3) except: db_size = 0 #Build context for front-end and render page context = { 'node_info': node_info, + 'active_count': active_count, 'total_channels': total_channels, 'balances': balances, 'total_balance': balances.total_balance + sum_outbound + pending_open_balance + limbo_balance + private_outbound, @@ -280,10 +309,10 @@ def home(request): '1day_payments_ppm': 0 if payments_1day_amt == 0 else int((total_1day_fees/payments_1day_amt)*1000000), '7day_payments_ppm': 0 if payments_7day_amt == 0 else int((total_7day_fees/payments_7day_amt)*1000000), 'liq_ratio': 0 if sum_outbound == 0 else int((sum_inbound/sum_outbound)*100), - 'eligible_count': channels.filter(is_active=True, is_open=True, private=False, auto_rebalance=True).annotate(inbound_can=((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity')).annotate(fee_ratio=(Sum('remote_fee_rate')*100)/Sum('local_fee_rate')).filter(inbound_can__gte=F('ar_in_target'), fee_ratio__lte=F('ar_max_cost')).count(), + 'eligible_count': eligible_count, 'enabled_count': channels.filter(is_open=True, auto_rebalance=True).count(), - 'available_count': channels.filter(is_active=True, is_open=True, private=False, auto_rebalance=False).annotate(outbound_can=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).filter(outbound_can__gte=F('ar_out_target')).count(), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'available_count': available_count, + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links(), 'network_links': network_links(), 'db_size': db_size, @@ -299,7 +328,7 @@ def home(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def channels(request): if request.method == 'GET': filter_7day = datetime.now() - timedelta(days=7) @@ -386,7 +415,7 @@ def channels(request): 'channels': [] if channels_df.empty else channels_df.sort_values(by=['cv_30day'], ascending=False).to_dict(orient='records'), 'apy_7day': apy_7day, 'apy_30day': apy_30day, - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links(), 'network_links': network_links() } @@ -394,7 +423,7 @@ def channels(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def fees(request): if request.method == 'GET': filter_1day = datetime.now() - timedelta(days=1) @@ -427,6 +456,11 @@ def fees(request): else: LocalSettings(key='AF-FailedHTLCs', value='25').save() failed_htlc_limit = 25 + if LocalSettings.objects.filter(key='AF-UpdateHours').exists(): + update_hours = int(LocalSettings.objects.filter(key='AF-UpdateHours')[0].value) + else: + LocalSettings(key='AF-UpdateHours', value='24').save() + update_hours = 24 failed_htlc_df = DataFrame.from_records(FailedHTLCs.objects.filter(timestamp__gte=filter_1day).order_by('-id').values()) if failed_htlc_df.shape[0] > 0: failed_htlc_df = failed_htlc_df[(failed_htlc_df['wire_failure']==15) & (failed_htlc_df['failure_detail']==6) & (failed_htlc_df['amount']>failed_htlc_df['chan_out_liq']+failed_htlc_df['chan_out_pending'])] @@ -475,11 +509,11 @@ def fees(request): channels_df['new_rate'] = channels_df.apply(lambda row: max_rate if max_rate < row['new_rate'] else row['new_rate'], axis=1) channels_df['new_rate'] = channels_df.apply(lambda row: min_rate if min_rate > row['new_rate'] else row['new_rate'], axis=1) channels_df['adjustment'] = channels_df.apply(lambda row: int(row['new_rate']-row['local_fee_rate']), axis=1) - channels_df['eligible'] = channels_df.apply(lambda row: (datetime.now()-row['fees_updated']).total_seconds() > 86400, axis=1) + channels_df['eligible'] = channels_df.apply(lambda row: (datetime.now()-row['fees_updated']).total_seconds() > (update_hours*3600), axis=1) context = { 'channels': [] if channels_df.empty else channels_df.sort_values(by=['out_percent']).to_dict(orient='records'), 'local_settings': LocalSettings.objects.filter(key__contains='AF-').order_by('key'), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links(), 'network_links': network_links() } @@ -487,7 +521,7 @@ def fees(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def advanced(request): if request.method == 'GET': channels = Channels.objects.filter(is_open=True).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*1000)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*1000)/Sum('capacity')).order_by('-is_active', 'outbound_percent') @@ -498,10 +532,12 @@ def advanced(request): channels_df['local_balance'] = channels_df.apply(lambda row: row.local_balance + row.pending_outbound, axis=1) channels_df['remote_balance'] = channels_df.apply(lambda row: row.remote_balance + row.pending_inbound, axis=1) channels_df['fee_ratio'] = channels_df.apply(lambda row: 100 if row['local_fee_rate'] == 0 else int(round(((row['remote_fee_rate']/row['local_fee_rate'])*1000)/10, 0)), axis=1) + channels_df['local_min_htlc'] = channels_df['local_min_htlc_msat']/1000 + channels_df['local_max_htlc'] = channels_df['local_max_htlc_msat']/1000 context = { 'channels': channels_df.to_dict(orient='records'), 'local_settings': LocalSettings.objects.all().order_by('key'), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links(), 'network_links': network_links() } @@ -509,19 +545,34 @@ def advanced(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) 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).annotate(ppm=Round((Sum('fee')/Sum('amt'))*1000000, output_field=IntegerField())) - } - return render(request, 'route.html', context) + try: + stub = lnrpc.LightningStub(lnd_connect()) + block_height = stub.GetInfo(ln.GetInfoRequest()).block_height + payment_hash = request.GET.urlencode()[1:] + route = PaymentHops.objects.filter(payment_hash=payment_hash).annotate(ppm=Round((Sum('fee')/Sum('amt'))*1000000, output_field=IntegerField())) if PaymentHops.objects.filter(payment_hash=payment_hash).exists() else None + total_cost = round(route.aggregate(Sum('fee'))['fee__sum'],3) if route is not None else 0 + total_ppm = int(total_cost*1000000/route.filter(step=1).aggregate(Sum('amt'))['amt__sum']) if route is not None else 0 + context = { + 'payment_hash': payment_hash, + 'total_cost': total_cost, + 'total_ppm': total_ppm, + 'route': route, + 'rebalances': Rebalancer.objects.filter(payment_hash=payment_hash).annotate(ppm=Round((Sum('fee_limit')*1000000)/Sum('value'), output_field=IntegerField())), + 'invoices': Invoices.objects.filter(r_hash=payment_hash), + 'incoming_htlcs': PendingHTLCs.objects.filter(incoming=True, hash_lock=payment_hash).annotate(blocks_til_expiration=Sum('expiration_height')-block_height).annotate(hours_til_expiration=((Sum('expiration_height')-block_height)*10)/60).order_by('hash_lock'), + 'outgoing_htlcs': PendingHTLCs.objects.filter(incoming=False, hash_lock=payment_hash).annotate(blocks_til_expiration=Sum('expiration_height')-block_height).annotate(hours_til_expiration=((Sum('expiration_height')-block_height)*10)/60).order_by('hash_lock') + } + return render(request, 'route.html', context) + except Exception as e: + error = str(e) + return render(request, 'error.html', {'error': error}) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def routes(request): if request.method == 'GET': try: @@ -537,39 +588,39 @@ def routes(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def peers(request): if request.method == 'GET': peers = Peers.objects.filter(connected=True) context = { 'peers': peers, 'num_peers': len(peers), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links() } return render(request, 'peers.html', context) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def balances(request): if request.method == 'GET': - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) 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')), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'network_links': network_links() } return render(request, 'balances.html', context) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def closures(request): if request.method == 'GET': try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) pending_channels = stub.PendingChannels(ln.PendingChannelsRequest()) channels = Channels.objects.all() pending_closed = None @@ -588,20 +639,19 @@ def closures(request): if closures_df.empty: merged = DataFrame() else: - channels_df = DataFrame.from_records(Channels.objects.all().values('chan_id', 'alias', 'closing_costs')) + channels_df = DataFrame.from_records(Channels.objects.all().values('chan_id', 'alias')) if channels_df.empty: merged = closures_df merged['alias'] = '' else: merged = merge(closures_df, channels_df, on='chan_id', how='left') merged['alias'] = merged['alias'].fillna('') - merged['closing_costs'] = merged['closing_costs'].fillna('') context = { 'pending_closed': pending_closed, 'pending_force_closed': pending_force_closed, 'waiting_for_close': waiting_for_close, 'closures': [] if merged.empty else merged.sort_values(by=['close_height'], ascending=False).to_dict(orient='records'), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'network_links': network_links(), 'graph_links': graph_links() } @@ -625,11 +675,11 @@ def find_next_block_maturity(force_closing_channel): return pending_htlc.blocks_til_maturity return -1 -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def towers(request): if request.method == 'GET': try: - stub = wtstub.WatchtowerClientStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = wtstub.WatchtowerClientStub(lnd_connect()) towers = stub.ListTowers(wtrpc.ListTowersRequest(include_sessions=False)).towers active_towers = [] inactive_towers = [] @@ -657,13 +707,13 @@ def towers(request): else: return redirect(request.META.get('HTTP_REFERER')) -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def add_tower_form(request): if request.method == 'POST': form = AddTowerForm(request.POST) if form.is_valid(): try: - stub = wtstub.WatchtowerClientStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = wtstub.WatchtowerClientStub(lnd_connect()) tower = form.cleaned_data['tower'] if tower.count('@') == 1 and len(tower.split('@')[0]) == 66: peer_pubkey, host = tower.split('@') @@ -681,13 +731,13 @@ def add_tower_form(request): messages.error(request, 'Invalid Request. Please try again.') return redirect(request.META.get('HTTP_REFERER')) -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def delete_tower_form(request): if request.method == 'POST': form = DeleteTowerForm(request.POST) if form.is_valid(): try: - stub = wtstub.WatchtowerClientStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = wtstub.WatchtowerClientStub(lnd_connect()) pubkey = bytes.fromhex(form.cleaned_data['pubkey']) address = form.cleaned_data['address'] response = stub.RemoveTower(wtrpc.RemoveTowerRequest(pubkey=pubkey, address=address)) @@ -702,13 +752,13 @@ def delete_tower_form(request): messages.error(request, 'Invalid Request. Please try again.') return redirect(request.META.get('HTTP_REFERER')) -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def remove_tower_form(request): if request.method == 'POST': form = RemoveTowerForm(request.POST) if form.is_valid(): try: - stub = wtstub.WatchtowerClientStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = wtstub.WatchtowerClientStub(lnd_connect()) pubkey = bytes.fromhex(form.cleaned_data['pubkey']) response = stub.RemoveTower(wtrpc.RemoveTowerRequest(pubkey=pubkey)) messages.success(request, 'Tower removal successful!' + str(response)) @@ -722,30 +772,34 @@ def remove_tower_form(request): messages.error(request, 'Invalid Request. Please try again.') return redirect(request.META.get('HTTP_REFERER')) -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def resolutions(request): if request.method == 'GET': chan_id = request.GET.urlencode()[1:] context = { 'chan_id': chan_id, 'resolutions': Resolutions.objects.filter(chan_id=chan_id), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'network_links': network_links() } return render(request, 'resolutions.html', context) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def income(request): if request.method == 'GET': - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) filter_90day = datetime.now() - timedelta(days=90) filter_30day = datetime.now() - timedelta(days=30) filter_7day = datetime.now() - timedelta(days=7) filter_1day = datetime.now() - timedelta(days=1) node_info = stub.GetInfo(ln.GetInfoRequest()) - channels = Channels.objects.all() + invoices = Invoices.objects.filter(state=1, is_revenue=True) + invoices_90day = invoices.filter(settle_date__gte=filter_90day) + invoices_30day = invoices.filter(settle_date__gte=filter_30day) + invoices_7day = invoices.filter(settle_date__gte=filter_7day) + invoices_1day = invoices.filter(settle_date__gte=filter_1day) payments = Payments.objects.filter(status=2) payments_90day = payments.filter(creation_date__gte=filter_90day) payments_30day = payments.filter(creation_date__gte=filter_30day) @@ -756,11 +810,11 @@ def income(request): onchain_txs_30day = onchain_txs.filter(time_stamp__gte=filter_30day) onchain_txs_7day = onchain_txs.filter(time_stamp__gte=filter_7day) onchain_txs_1day = onchain_txs.filter(time_stamp__gte=filter_1day) - closures = channels.filter(chan_id__in=Closures.objects.all().values('chan_id')) - closures_90day = channels.filter(chan_id__in=Closures.objects.filter(close_height__gte=(node_info.block_height - 12960)).values('chan_id')) - closures_30day = channels.filter(chan_id__in=Closures.objects.filter(close_height__gte=(node_info.block_height - 4320)).values('chan_id')) - closures_7day = channels.filter(chan_id__in=Closures.objects.filter(close_height__gte=(node_info.block_height - 1008)).values('chan_id')) - closures_1day = channels.filter(chan_id__in=Closures.objects.filter(close_height__gte=(node_info.block_height - 144)).values('chan_id')) + closures = Closures.objects.all() + closures_90day = Closures.objects.filter(close_height__gte=(node_info.block_height - 12960)) + closures_30day = Closures.objects.filter(close_height__gte=(node_info.block_height - 4320)) + closures_7day = Closures.objects.filter(close_height__gte=(node_info.block_height - 1008)) + closures_1day = Closures.objects.filter(close_height__gte=(node_info.block_height - 144)) forwards = Forwards.objects.all() forwards_90day = forwards.filter(forward_date__gte=filter_90day) forwards_30day = forwards.filter(forward_date__gte=filter_30day) @@ -781,6 +835,16 @@ def income(request): total_revenue_30day = 0 if forward_count_30day == 0 else int(forwards_30day.aggregate(Sum('fee'))['fee__sum']) total_revenue_7day = 0 if forward_count_7day == 0 else int(forwards_7day.aggregate(Sum('fee'))['fee__sum']) total_revenue_1day = 0 if forward_count_1day == 0 else int(forwards_1day.aggregate(Sum('fee'))['fee__sum']) + total_received = 0 if invoices.count() == 0 else int(invoices.aggregate(Sum('amt_paid'))['amt_paid__sum']) + total_received_90day = 0 if invoices_90day.count() == 0 else int(invoices_90day.aggregate(Sum('amt_paid'))['amt_paid__sum']) + total_received_30day = 0 if invoices_30day.count() == 0 else int(invoices_30day.aggregate(Sum('amt_paid'))['amt_paid__sum']) + total_received_7day = 0 if invoices_7day.count() == 0 else int(invoices_7day.aggregate(Sum('amt_paid'))['amt_paid__sum']) + total_received_1day = 0 if invoices_1day.count() == 0 else int(invoices_1day.aggregate(Sum('amt_paid'))['amt_paid__sum']) + total_revenue += total_received + total_revenue_90day += total_received_90day + total_revenue_30day += total_received_30day + total_revenue_7day += total_received_7day + total_revenue_1day += total_received_1day total_revenue_ppm = 0 if forward_amount == 0 else int(total_revenue/(forward_amount/1000000)) total_revenue_ppm_90day = 0 if forward_amount_90day == 0 else int(total_revenue_90day/(forward_amount_90day/1000000)) total_revenue_ppm_30day = 0 if forward_amount_30day == 0 else int(total_revenue_30day/(forward_amount_30day/1000000)) @@ -873,14 +937,14 @@ def income(request): 'percent_cost_30day': 0 if total_revenue_30day == 0 else int(((total_fees_30day+onchain_costs_30day)/total_revenue_30day)*100), 'percent_cost_7day': 0 if total_revenue_7day == 0 else int(((total_fees_7day+onchain_costs_7day)/total_revenue_7day)*100), 'percent_cost_1day': 0 if total_revenue_1day == 0 else int(((total_fees_1day+onchain_costs_1day)/total_revenue_1day)*100), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links() } return render(request, 'income.html', context) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def channel(request): if request.method == 'GET': chan_id = request.GET.urlencode()[1:] @@ -901,6 +965,7 @@ def channel(request): channels_df = DataFrame.from_records(Channels.objects.filter(chan_id=chan_id).values()) rebalancer_df = DataFrame.from_records(Rebalancer.objects.filter(last_hop_pubkey=channels_df['remote_pubkey'][0]).annotate(ppm=Round((Sum('fee_limit')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-id').values()) failed_htlc_df = DataFrame.from_records(FailedHTLCs.objects.filter(Q(chan_id_in=chan_id) | Q(chan_id_out=chan_id)).order_by('-id').values()) + peer_info_df = DataFrame.from_records(Peers.objects.filter(pubkey=channels_df['remote_pubkey'][0]).values()) channels_df['local_balance'] = channels_df['local_balance'] + channels_df['pending_outbound'] channels_df['remote_balance'] = channels_df['remote_balance'] + channels_df['pending_inbound'] channels_df['in_percent'] = int(round((channels_df['remote_balance']/channels_df['capacity'])*100, 0)) @@ -1132,20 +1197,20 @@ def channel(request): channels_df['amt_rebal_in_30day'] = int(invoices_df_30d_sum.loc[chan_id].amt_paid) if invoices_df_30d_count.empty == False else 0 channels_df['amt_rebal_in_7day'] = int(invoices_df_7d_sum.loc[chan_id].amt_paid) if invoices_df_7d_count.empty == False else 0 channels_df['amt_rebal_in_1day'] = int(invoices_df_1d_sum.loc[chan_id].amt_paid) if invoices_df_1d_count.empty == False else 0 - rebal_payments_df = DataFrame.from_records(Payments.objects.filter(status=2).filter(rebal_chan=chan_id).values()) + rebal_payments_df = DataFrame.from_records(Payments.objects.filter(status=2).filter(value__gte=1000.0).filter(rebal_chan=chan_id).values()) if rebal_payments_df.shape[0] > 0: rebal_payments_df_30d = rebal_payments_df.loc[rebal_payments_df['creation_date'] >= filter_30day] rebal_payments_df_7d = rebal_payments_df_30d.loc[rebal_payments_df_30d['creation_date'] >= filter_7day] rebal_payments_df_1d = rebal_payments_df_7d.loc[rebal_payments_df_7d['creation_date'] >= filter_1day] - invoice_hashes = DataFrame() if invoices_df.empty else invoices_df.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) - 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_1d = DataFrame() if invoices_df_1d.empty else invoices_df_1d.groupby('chan_in', as_index=True)['r_hash'].apply(list) + invoice_hashes = DataFrame() if invoices_df.empty else invoices_df.loc[invoices_df['value'] >= 1000].groupby('chan_in', as_index=True)['r_hash'].apply(list) + invoice_hashes_30d = DataFrame() if invoices_df_30d.empty else invoices_df_30d.loc[invoices_df_30d['value'] >= 1000].groupby('chan_in', as_index=True)['r_hash'].apply(list) + invoice_hashes_7d = DataFrame() if invoices_df_7d.empty else invoices_df_7d.loc[invoices_df_7d['value'] >= 1000].groupby('chan_in', as_index=True)['r_hash'].apply(list) + invoice_hashes_1d = DataFrame() if invoices_df_1d.empty else invoices_df_1d.loc[invoices_df_1d['value'] >= 1000].groupby('chan_in', as_index=True)['r_hash'].apply(list) channels_df['costs'] = 0 if channels_df['rebal_in'][0] == 0 or invoice_hashes.empty == True else int(rebal_payments_df.set_index('payment_hash', inplace=False).loc[invoice_hashes[chan_id]]['fee'].sum()) channels_df['costs_30day'] = 0 if channels_df['rebal_in_30day'][0] == 0 or invoice_hashes_30d.empty == True else int(rebal_payments_df_30d.set_index('payment_hash', inplace=False).loc[invoice_hashes_30d[chan_id]]['fee'].sum()) channels_df['costs_7day'] = 0 if channels_df['rebal_in_7day'][0] == 0 or invoice_hashes_7d.empty == True else int(rebal_payments_df_7d.set_index('payment_hash', inplace=False).loc[invoice_hashes_7d[chan_id]]['fee'].sum()) channels_df['costs_1day'] = 0 if channels_df['rebal_in_1day'][0] == 0 or invoice_hashes_1d.empty == True else int(rebal_payments_df_1d.set_index('payment_hash', inplace=False).loc[invoice_hashes_1d[chan_id]]['fee'].sum()) - channels_df['costs'] += channels_df['closing_costs'] + channels_df['costs'] += Closures.objects.filter(funding_txid=channels_df['funding_txid'][0],funding_index=channels_df['output_index'][0])[0].closing_costs if Closures.objects.filter(funding_txid=channels_df['funding_txid'][0],funding_index=channels_df['output_index'][0]).exists() else 0 channels_df['profits'] = channels_df['revenue'] - channels_df['costs'] channels_df['profits_30day'] = channels_df['revenue_30day'] - channels_df['costs_30day'] channels_df['profits_7day'] = channels_df['revenue_7day'] - channels_df['costs_7day'] @@ -1238,6 +1303,9 @@ def channel(request): channels_df['cv_30day'] = round((channels_df['revenue_30day']*1216.6667)/(channels_df['capacity']*outbound_ratio) + channels_df['assisted_apy_30day'], 2) channels_df['cv_7day'] = round((channels_df['revenue_7day']*5214.2857)/(channels_df['capacity']*outbound_ratio) + channels_df['assisted_apy_7day'], 2) channels_df['cv_1day'] = round((channels_df['revenue_1day']*36500)/(channels_df['capacity']*outbound_ratio) + channels_df['assisted_apy_1day'], 2) + autofees_df = DataFrame.from_records(Autofees.objects.filter(chan_id=chan_id).filter(timestamp__gte=filter_30day).order_by('-id').values()) + if autofees_df.shape[0]> 0: + autofees_df['change'] = autofees_df.apply(lambda row: 0 if row.old_value == 0 else round((row.new_value-row.old_value)*100/row.old_value, 1), axis=1) else: channels_df = DataFrame() forwards_df = DataFrame() @@ -1245,6 +1313,8 @@ def channel(request): invoices_df = DataFrame() rebalancer_df = DataFrame() failed_htlc_df = DataFrame() + peer_info_df = DataFrame() + autofees_df = DataFrame() context = { 'chan_id': chan_id, 'channel': [] if channels_df.empty else channels_df.to_dict(orient='records')[0], @@ -1255,33 +1325,46 @@ def channel(request): 'invoices': [] if invoices_df.empty else invoices_df.sort_values(by=['settle_date'], ascending=False).to_dict(orient='records')[:5], 'rebalances': [] if rebalancer_df.empty else rebalancer_df.to_dict(orient='records')[:5], 'failed_htlcs': [] if failed_htlc_df.empty else failed_htlc_df.to_dict(orient='records')[:5], - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'peer_info': [] if peer_info_df.empty else peer_info_df.to_dict(orient='records')[0], + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links(), - 'network_links': network_links() + 'network_links': network_links(), + 'autofees': [] if autofees_df.empty else autofees_df.to_dict(orient='records') } - return render(request, 'channel.html', context) + try: + return render(request, 'channel.html', context) + except Exception as e: + try: + error = str(e.code()) + except: + error = str(e) + return render(request, 'error.html', {'error': error}) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def opens(request): if request.method == 'GET': - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey current_peers = Channels.objects.filter(is_open=True).values_list('remote_pubkey') + exlcude_list = AvoidNodes.objects.values_list('pubkey') filter_60day = datetime.now() - timedelta(days=60) payments_60day = Payments.objects.filter(creation_date__gte=filter_60day).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)).exclude(score=0).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).exclude(node_pubkey__in=exlcude_list).values('node_pubkey').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)).exclude(score=0).order_by('-score', 'ppm')[:21] + for open in open_list: + open['alias'] = PaymentHops.objects.filter(node_pubkey=open['node_pubkey']).order_by('-id')[0].alias context = { 'open_list': open_list, - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'avoid_list': AvoidNodes.objects.all(), + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links() } return render(request, 'open_list.html', context) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def actions(request): if request.method == 'GET': channels = Channels.objects.filter(is_active=True, is_open=True, private=False).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*1000)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*1000)/Sum('capacity')) @@ -1334,7 +1417,7 @@ def actions(request): action_list.append(result) context = { 'action_list': action_list, - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links(), 'network_links': network_links() } @@ -1342,10 +1425,10 @@ def actions(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def pending_htlcs(request): if request.method == 'GET': - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) block_height = stub.GetInfo(ln.GetInfoRequest()).block_height context = { 'incoming_htlcs': PendingHTLCs.objects.filter(incoming=True).annotate(blocks_til_expiration=Sum('expiration_height')-block_height).annotate(hours_til_expiration=((Sum('expiration_height')-block_height)*10)/60).order_by('hash_lock'), @@ -1355,27 +1438,41 @@ def pending_htlcs(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def failed_htlcs(request): if request.method == 'GET': - context = { - 'failed_htlcs': FailedHTLCs.objects.all().order_by('-id')[:150], - } - return render(request, 'failed_htlcs.html', context) + try: + #print (f"{datetime.now().strftime('%c')} : {request.GET.urlencode()=}") + query = None if request.GET.urlencode()[1:] == '' else request.GET.urlencode()[1:].split('_') + chan_id = None if query is None or len(query) < 1 else query[0] + direction = None if query is None or len(query) < 2 else query[1] + #print (f"{datetime.now().strftime('%c')} : {query=} {chan_id=} {direction=}") + failed_htlcs=FailedHTLCs.objects.all().order_by('-id')[:150] if chan_id is None else (FailedHTLCs.objects.filter(chan_id_out=chan_id).order_by('-id')[:150] if direction == "O" else FailedHTLCs.objects.filter(chan_id_in=chan_id).order_by('-id')[:150]) + + context = { + 'failed_htlcs': failed_htlcs + } + return render(request, 'failed_htlcs.html', context) + except Exception as e: + try: + error = str(e.code()) + except: + error = str(e) + return render(request, 'error.html', {'error': error}) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def payments(request): if request.method == 'GET': context = { - 'payments': Payments.objects.filter(status=2).annotate(ppm=Round((Sum('fee')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-creation_date')[:150], + 'payments': Payments.objects.exclude(status=3).annotate(ppm=Round((Sum('fee')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-creation_date')[:150], } return render(request, 'payments.html', context) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def invoices(request): if request.method == 'GET': context = { @@ -1385,17 +1482,27 @@ def invoices(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def rebalances(request): if request.method == 'GET': - context = { - 'rebalances': Rebalancer.objects.all().annotate(ppm=Round((Sum('fee_limit')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-id')[:150], - } - return render(request, 'rebalances.html', context) + try: + rebalances = Rebalancer.objects.all().annotate(ppm=Round((Sum('fee_limit')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-id') + rebalances_success = rebalances.filter(status=2) + context = { + 'rebalances': rebalances[:150], + 'rebalances_success' : rebalances_success[:69] + } + return render(request, 'rebalances.html', context) + except Exception as e: + try: + error = str(e.code()) + except: + error = str(e) + return render(request, 'error.html', {'error': error}) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def batch(request): if request.method == 'GET': context = { @@ -1418,7 +1525,7 @@ def open_peer(peer_pubkey, stub): except: return False -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def batch_open(request): if request.method == 'POST': form = BatchOpenForm(request.POST) @@ -1426,7 +1533,7 @@ def batch_open(request): count = 0 fail = False open_list = [] - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) if form.cleaned_data['pubkey1'] and form.cleaned_data['amt1'] and len(form.cleaned_data['pubkey1']) == 66: count += 1 peer_pubkey = form.cleaned_data['pubkey1'] @@ -1542,7 +1649,7 @@ def batch_open(request): messages.error(request, 'Invalid Request. Please try again.') return redirect('batch') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def forwards(request): if request.method == 'GET': context = { @@ -1552,14 +1659,16 @@ def forwards(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def rebalancing(request): if request.method == 'GET': + channels_df = DataFrame.from_records(Channels.objects.filter(is_open=True, private=False).annotate(percent_inbound=((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity')).annotate(percent_outbound=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).order_by('-is_active', 'percent_outbound').values()) filter_7day = datetime.now() - timedelta(days=7) rebalancer_7d_df = DataFrame.from_records(Rebalancer.objects.filter(stop__gte=filter_7day).order_by('-id').values()) - channels_df = DataFrame.from_records(Channels.objects.filter(is_open=True, private=False).annotate(percent_inbound=((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity')).annotate(percent_outbound=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).order_by('-is_active', 'percent_outbound').values()) if channels_df.shape[0] > 0: channels_df['inbound_can'] = channels_df['percent_inbound'] / channels_df['ar_in_target'] + channels_df['local_balance'] = channels_df['local_balance'] + channels_df['pending_outbound'] + channels_df['remote_balance'] = channels_df['remote_balance'] + channels_df['pending_inbound'] channels_df['fee_ratio'] = channels_df.apply(lambda row: 100 if row['local_fee_rate'] == 0 else int(round(((row['remote_fee_rate']/row['local_fee_rate'])*1000)/10, 0)), axis=1) channels_df['fee_check'] = channels_df.apply(lambda row: 1 if row['ar_max_cost'] == 0 else int(round(((row['fee_ratio']/row['ar_max_cost'])*1000)/10, 0)), axis=1) channels_df['steps'] = channels_df.apply(lambda row: 0 if row['inbound_can'] < 1 else int(((row['percent_inbound']-row['ar_in_target'])/((row['ar_amt_target']/row['capacity'])*100))+0.999), axis=1) @@ -1577,6 +1686,24 @@ def rebalancing(request): eligible_count = 0 enabled_count = 0 available_count = 0 + try: + query = request.GET.urlencode()[1:] + if query == '1': + #Filter Sink (AR Enabled) + channels_df = channels_df[channels_df['auto_rebalance']==True][channels_df['is_active']==True] + elif query == '2': + #Filter Source (Eligible to rebalance out) + channels_df = channels_df[channels_df['auto_rebalance']==False][channels_df['is_active']==True].sort_values(by=['percent_outbound'], ascending=False) + else: + #Proceed + pass + except Exception as e: + try: + error = str(e.code()) + except: + error = str(e) + return render(request, 'error.html', {'error': error}) + context = { 'eligible_count': eligible_count, 'enabled_count': enabled_count, @@ -1585,14 +1712,14 @@ def rebalancing(request): 'rebalancer': Rebalancer.objects.all().annotate(ppm=Round((Sum('fee_limit')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-id')[:20], 'rebalancer_form': RebalancerForm, 'local_settings': LocalSettings.objects.filter(key__contains='AR-').order_by('key'), - 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network': 'testnet/' if settings.LND_NETWORK == 'testnet' else '', 'graph_links': graph_links() } return render(request, 'rebalancing.html', context) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def keysends(request): if request.method == 'GET': context = { @@ -1602,33 +1729,49 @@ def keysends(request): else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def autopilot(request): if request.method == 'GET': + chan_id = request.GET.urlencode()[1:] + filter_21d = datetime.now() - timedelta(days=21) + autopilot = Autopilot.objects.filter(timestamp__gte=filter_21d).order_by('-id') if chan_id == "" else Autopilot.objects.filter(chan_id = chan_id).filter(timestamp__gte=filter_21d).order_by('-id') context = { - 'autopilot': Autopilot.objects.all().order_by('-id') + 'autopilot': autopilot } return render(request, 'autopilot.html', context) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def autofees(request): if request.method == 'GET': - context = { - 'autofees': Autofees.objects.all().order_by('-id') - } - return render(request, 'autofees.html', context) + chan_id = request.GET.urlencode()[1:] + filter_7d = datetime.now() - timedelta(days=7) + autofees_df = DataFrame.from_records(Autofees.objects.filter(timestamp__gte=filter_7d).order_by('-id').values() if chan_id == "" else Autofees.objects.filter(chan_id=chan_id).filter(timestamp__gte=filter_7d).order_by('-id').values()) + if autofees_df.shape[0]> 0: + autofees_df['change'] = autofees_df.apply(lambda row: 0 if row.old_value == 0 else round((row.new_value-row.old_value)*100/row.old_value, 1), axis=1) + #print (f"{datetime.now().strftime('%c')} : {chan_id=} {autofees=}") + try: + context = { + 'autofees': [] if autofees_df.empty else autofees_df.to_dict(orient='records') + } + return render(request, 'autofees.html', context) + except Exception as e: + try: + error = str(e.code()) + except: + error = str(e) + return render(request, 'error.html', {'error': error}) else: return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def open_channel_form(request): if request.method == 'POST': form = OpenChannelForm(request.POST) if form.is_valid(): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) peer_pubkey = form.cleaned_data['peer_pubkey'] connected = False if Peers.objects.filter(pubkey=peer_pubkey, connected=True).exists(): @@ -1660,7 +1803,7 @@ def open_channel_form(request): messages.error(request, 'Invalid Request. Please try again.') return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def close_channel_form(request): if request.method == 'POST': form = CloseChannelForm(request.POST) @@ -1676,7 +1819,7 @@ def close_channel_form(request): channel_point.funding_txid_bytes = bytes.fromhex(funding_txid) channel_point.funding_txid_str = funding_txid channel_point.output_index = output_index - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) if form.cleaned_data['force']: for response in stub.CloseChannel(ln.CloseChannelRequest(channel_point=channel_point, force=True)): messages.success(request, 'Channel force closed! Closing TXID: ' + str(response.close_pending.txid[::-1].hex()) + ':' + str(response.close_pending.output_index)) @@ -1697,13 +1840,13 @@ def close_channel_form(request): messages.error(request, 'Invalid Request. Please try again.') return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def connect_peer_form(request): if request.method == 'POST': form = ConnectPeerForm(request.POST) if form.is_valid(): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) peer_id = form.cleaned_data['peer_id'] if peer_id.count('@') == 0 and len(peer_id) == 66: peer_pubkey = peer_id @@ -1726,12 +1869,17 @@ def connect_peer_form(request): messages.error(request, 'Invalid Request. Please try again.') return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def new_address_form(request): if request.method == 'POST': try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - response = stub.NewAddress(ln.NewAddressRequest(type=0)) + stub = lnrpc.LightningStub(lnd_connect()) + version = stub.GetInfo(ln.GetInfoRequest()).version + # Verify sufficient version to handle p2tr address creation + if float(version[:4]) >= 0.15: + response = stub.NewAddress(ln.NewAddressRequest(type=4)) + else: + response = stub.NewAddress(ln.NewAddressRequest(type=0)) messages.success(request, 'Deposit Address: ' + str(response.address)) except Exception as e: error = str(e) @@ -1741,13 +1889,13 @@ def new_address_form(request): messages.error(request, 'Address request failed! Error: ' + error_msg) return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def add_invoice_form(request): if request.method == 'POST': form = AddInvoiceForm(request.POST) if form.is_valid(): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) response = stub.AddInvoice(ln.Invoice(value=form.cleaned_data['value'])) messages.success(request, 'Invoice created! ' + str(response.payment_request)) except Exception as e: @@ -1760,7 +1908,7 @@ def add_invoice_form(request): messages.error(request, 'Invalid Request. Please try again.') return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def rebalance(request): if request.method == 'POST': form = RebalancerForm(request.POST) @@ -1789,14 +1937,14 @@ def rebalance(request): messages.error(request, 'Invalid Request. Please try again.') return redirect(request.META.get('HTTP_REFERER')) -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def update_chan_policy(request): if request.method == 'POST': form = ChanPolicyForm(request.POST) if form.is_valid(): if form.cleaned_data['new_base_fee'] is not None or form.cleaned_data['new_fee_rate'] is not None or form.cleaned_data['new_cltv'] is not None: try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) if form.cleaned_data['target_all']: if form.cleaned_data['new_base_fee'] is not None and form.cleaned_data['new_fee_rate'] is not None and form.cleaned_data['new_cltv'] is not None: args = {'global': True, 'base_fee_msat': form.cleaned_data['new_base_fee'], 'fee_rate': (form.cleaned_data['new_fee_rate']/1000000), 'time_lock_delta': form.cleaned_data['new_cltv']} @@ -1821,10 +1969,13 @@ def update_chan_policy(request): stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=new_base_fee, fee_rate=new_fee_rate, time_lock_delta=new_cltv)) db_channel = Channels.objects.get(chan_id=channel.chan_id) db_channel.local_base_fee = new_base_fee + old_fee_rate = db_channel.local_fee_rate db_channel.local_fee_rate = new_fee_rate*1000000 db_channel.local_cltv = new_cltv if form.cleaned_data['new_fee_rate'] is not None: db_channel.fees_updated = datetime.now() + Autofees(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, setting=(f"Manual"), old_value=old_fee_rate, new_value=db_channel.local_fee_rate).save() + db_channel.save() else: messages.error(request, 'No channels were specified in the update request!') @@ -1842,7 +1993,7 @@ def update_chan_policy(request): messages.error(request, 'Invalid Request. Please try again.') return redirect('home') -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def auto_rebalance(request): if request.method == 'POST': form = AutoRebalanceForm(request.POST) @@ -1987,7 +2138,7 @@ def auto_rebalance(request): messages.error(request, 'Invalid Request. Please try again.') return redirect(request.META.get('HTTP_REFERER')) -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def update_channel(request): if request.method == 'POST': form = UpdateChannel(request.POST) @@ -1997,7 +2148,7 @@ def update_channel(request): update_target = int(form.cleaned_data['update_target']) db_channel = Channels.objects.filter(chan_id=chan_id)[0] if update_target == 0: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) channel_point = ln.ChannelPoint() channel_point.funding_txid_bytes = bytes.fromhex(db_channel.funding_txid) channel_point.funding_txid_str = db_channel.funding_txid @@ -2007,15 +2158,17 @@ def update_channel(request): 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)) + stub = lnrpc.LightningStub(lnd_connect()) 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=db_channel.local_cltv)) + old_fee_rate = db_channel.local_fee_rate db_channel.local_fee_rate = target db_channel.fees_updated = datetime.now() db_channel.save() + Autofees(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, setting=(f"Manual"), old_value=old_fee_rate, new_value=db_channel.local_fee_rate).save() messages.success(request, 'Fee rate for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target)) elif update_target == 2: db_channel.ar_amt_target = target @@ -2038,7 +2191,7 @@ def update_channel(request): db_channel.save() messages.success(request, 'Auto rebalancer max cost for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target) + '%') elif update_target == 7: - stub = lnrouter.RouterStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrouter.RouterStub(lnd_connect()) channel_point = ln.ChannelPoint() channel_point.funding_txid_bytes = bytes.fromhex(db_channel.funding_txid) channel_point.funding_txid_str = db_channel.funding_txid @@ -2054,7 +2207,7 @@ def update_channel(request): db_channel.save() messages.success(request, 'Auto fees status for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(db_channel.auto_fees)) elif update_target == 9: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) channel_point = ln.ChannelPoint() channel_point.funding_txid_bytes = bytes.fromhex(db_channel.funding_txid) channel_point.funding_txid_str = db_channel.funding_txid @@ -2062,18 +2215,34 @@ def update_channel(request): stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=db_channel.local_base_fee, fee_rate=(db_channel.local_fee_rate/1000000), time_lock_delta=target)) db_channel.local_cltv = target db_channel.save() - messages.success(request, 'CLTV for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target)) + messages.success(request, 'CLTV for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(float(target))) elif update_target == 10: - db_channel.closing_costs = target + stub = lnrpc.LightningStub(lnd_connect()) + 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=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, min_htlc_msat_specified=True, min_htlc_msat=int(target*1000))) + db_channel.local_min_htlc_msat = int(target*1000) + db_channel.save() + messages.success(request, 'Min HTLC for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(float(target))) + elif update_target == 11: + stub = lnrpc.LightningStub(lnd_connect()) + channel_point = 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=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, max_htlc_msat=int(target*1000))) + db_channel.local_max_htlc_msat = int(target*1000) db_channel.save() - messages.success(request, 'Closing costs for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(db_channel.closing_costs)) + messages.success(request, 'Max HTLC for channel ' + str(db_channel.alias) + ' (' + str(db_channel.chan_id) + ') updated to a value of: ' + str(target)) else: messages.error(request, 'Invalid target code. Please try again.') else: messages.error(request, 'Invalid Request. Please try again.') return redirect(request.META.get('HTTP_REFERER')) -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def update_pending(request): if request.method == 'POST': form = UpdatePending(request.POST) @@ -2129,7 +2298,7 @@ def update_pending(request): messages.error(request, 'Invalid Request. Please try again.') return redirect(request.META.get('HTTP_REFERER')) -@login_required(login_url='/lndg-admin/login/?next=/') +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def update_setting(request): if request.method == 'POST': form = UpdateSetting(request.POST) @@ -2288,7 +2457,7 @@ def update_setting(request): messages.success(request, 'Updated payment cleanup retention days to: ' + str(retention_days)) elif key == 'ALL-oRate': target = int(value) - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) channels = Channels.objects.filter(is_open=True) for db_channel in channels: channel_point = ln.ChannelPoint() @@ -2296,16 +2465,18 @@ def update_setting(request): 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=db_channel.local_cltv)) + old_fee_rate = db_channel.local_fee_rate db_channel.local_fee_rate = target db_channel.fees_updated = datetime.now() db_channel.save() + Autofees(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, setting=(f"Manual"), old_value=old_fee_rate, new_value=db_channel.local_fee_rate).save() messages.success(request, 'Fee rate for all open channels updated to a value of: ' + str(target)) elif key == 'ALL-oBase': target = int(value) - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) channels = Channels.objects.filter(is_open=True) for db_channel in channels: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) channel_point = ln.ChannelPoint() channel_point.funding_txid_bytes = bytes.fromhex(db_channel.funding_txid) channel_point.funding_txid_str = db_channel.funding_txid @@ -2316,10 +2487,10 @@ def update_setting(request): messages.success(request, 'Base fee for all channels updated to a value of: ' + str(target)) elif key == 'ALL-CLTV': target = int(value) - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) channels = Channels.objects.filter(is_open=True) for db_channel in channels: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) channel_point = ln.ChannelPoint() channel_point.funding_txid_bytes = bytes.fromhex(db_channel.funding_txid) channel_point.funding_txid_str = db_channel.funding_txid @@ -2328,6 +2499,20 @@ def update_setting(request): db_channel.local_cltv = target db_channel.save() messages.success(request, 'CLTV for all channels updated to a value of: ' + str(target)) + elif key == 'ALL-minHTLC': + target = int(float(value)*1000) + stub = lnrpc.LightningStub(lnd_connect()) + channels = Channels.objects.filter(is_open=True) + for db_channel in channels: + stub = lnrpc.LightningStub(lnd_connect()) + 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=(db_channel.local_fee_rate/1000000), time_lock_delta=db_channel.local_cltv, min_htlc_msat_specified=True, min_htlc_msat=target)) + db_channel.local_min_htlc_msat = target + db_channel.save() + messages.success(request, 'Min HTLC for all channels updated to a value of: ' + str(float(value))) elif key == 'ALL-Amts': target = int(value) channels = Channels.objects.filter(is_open=True).update(ar_amt_target=target) @@ -2412,53 +2597,147 @@ def update_setting(request): db_enabled.value = enabled db_enabled.save() messages.success(request, 'Updated autofees daily failed HTLC trigger limit setting to: ' + str(enabled)) + elif key == 'AF-UpdateHours': + enabled = int(value) + try: + db_enabled = LocalSettings.objects.get(key='AF-UpdateHours') + except: + LocalSettings(key='AF-UpdateHours', value='24').save() + db_enabled = LocalSettings.objects.get(key='AF-UpdateHours') + db_enabled.value = enabled + db_enabled.save() + messages.success(request, 'Updated autofees update hours setting to: ' + str(enabled)) else: - messages.error(request, 'Invalid Request. Please try again.') + messages.error(request, 'Invalid Request. Please try again. [' + key +']') + else: + messages.error(request, 'Invalid Request Form. Please try again.') + return redirect(request.META.get('HTTP_REFERER')) + +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) +def update_closing(request): + if request.method == 'POST': + form = UpdateClosing(request.POST) + if form.is_valid() and Closures.objects.filter(funding_txid=form.cleaned_data['funding_txid'], funding_index=form.cleaned_data['funding_index']).exists(): + funding_txid = form.cleaned_data['funding_txid'] + funding_index = form.cleaned_data['funding_index'] + target = int(form.cleaned_data['target']) + db_closing = Closures.objects.filter(funding_txid=funding_txid, funding_index=funding_index)[0] + db_closing.closing_costs = target + db_closing.save() + messages.success(request, 'Updated closing costs for ' + str(funding_txid) + ':' + str(funding_index) + ' updated to a value of: ' + str(target)) + else: + messages.error(request, 'Invalid Request. Please try again.') + return redirect(request.META.get('HTTP_REFERER')) + +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) +def update_keysend(request): + if request.method == 'POST': + form = UpdateKeysend(request.POST) + if form.is_valid() and Invoices.objects.filter(r_hash=form.cleaned_data['r_hash']).exists(): + r_hash = form.cleaned_data['r_hash'] + db_invoice = Invoices.objects.filter(r_hash=r_hash)[0] + db_invoice.is_revenue = not db_invoice.is_revenue + db_invoice.save() + messages.success(request, ('Marked' if db_invoice.is_revenue else 'Unmarked') + ' invoice ' + str(r_hash) + ' as revenue.') + else: + messages.error(request, 'Invalid Request. Please try again.') + return redirect(request.META.get('HTTP_REFERER')) + +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) +def add_avoid(request): + if request.method == 'POST': + form = AddAvoid(request.POST) + if form.is_valid(): + pubkey = form.cleaned_data['pubkey'] + notes = form.cleaned_data['notes'] + AvoidNodes(pubkey=pubkey, notes=notes).save() + messages.success(request, 'Successfully added node ' + str(pubkey) + ' to the avoid list.') + else: + messages.error(request, 'Invalid Request. Please try again.') + return redirect(request.META.get('HTTP_REFERER')) + +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) +def remove_avoid(request): + if request.method == 'POST': + form = RemoveAvoid(request.POST) + if form.is_valid() and AvoidNodes.objects.filter(pubkey=form.cleaned_data['pubkey']).exists(): + pubkey = form.cleaned_data['pubkey'] + AvoidNodes.objects.filter(pubkey=pubkey).delete() + messages.success(request, 'Successfully removed node ' + str(pubkey) + ' from the avoid list.') else: messages.error(request, 'Invalid Request. Please try again.') return redirect(request.META.get('HTTP_REFERER')) +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) +def get_fees(request): + if request.method == 'GET': + missing_fees = Closures.objects.exclude(close_type__in=[4, 5]).exclude(open_initiator=2, resolution_count=0).filter(closing_costs=0) + if missing_fees: + for missing_fee in missing_fees: + try: + txid = missing_fee.closing_tx + closing_costs = get_tx_fees(txid) if missing_fee.open_initiator == 1 else 0 + for resolution in Resolutions.objects.filter(chan_id=missing_fee.chan_id).exclude(resolution_type=2): + closing_costs += get_tx_fees(resolution.sweep_txid) + missing_fee.closing_costs = closing_costs + missing_fee.save() + except Exception as error: + messages.error(request, f"Error getting closure fees: {txid=} {error=}") + return redirect(request.META.get('HTTP_REFERER')) + return redirect(request.META.get('HTTP_REFERER')) + class PaymentsViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Payments.objects.all() serializer_class = PaymentSerializer class PaymentHopsViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = PaymentHops.objects.all() serializer_class = PaymentHopsSerializer class InvoicesViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Invoices.objects.all() serializer_class = InvoiceSerializer class ForwardsViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Forwards.objects.all() serializer_class = ForwardSerializer class PeersViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Peers.objects.all() serializer_class = PeerSerializer class OnchainViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Onchain.objects.all() serializer_class = OnchainSerializer class ClosuresViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Closures.objects.all() serializer_class = ClosuresSerializer class ResolutionsViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Resolutions.objects.all() serializer_class = ResolutionsSerializer class PendingHTLCViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = PendingHTLCs.objects.all() serializer_class = PendingHTLCSerializer class FailedHTLCViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = FailedHTLCs.objects.all() serializer_class = FailedHTLCSerializer class LocalSettingsViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = LocalSettings.objects.all() serializer_class = LocalSettingsSerializer @@ -2472,6 +2751,7 @@ def update(self, request, pk=None): return Response(serializer.errors) class ChannelsViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Channels.objects.all() serializer_class = ChannelSerializer @@ -2485,6 +2765,7 @@ def update(self, request, pk=None): return Response(serializer.errors) class RebalancerViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Rebalancer.objects.all() serializer_class = RebalancerSerializer @@ -2497,11 +2778,12 @@ def create(self, request): return Response(serializer.errors) @api_view(['POST']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def connect_peer(request): serializer = ConnectPeerSerializer(data=request.data) if serializer.is_valid(): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) peer_id = serializer.validated_data['peer_id'] if peer_id.count('@') == 0 and len(peer_id) == 66: peer_pubkey = peer_id @@ -2524,11 +2806,12 @@ def connect_peer(request): return Response({'error': 'Invalid request!'}) @api_view(['POST']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def open_channel(request): serializer = OpenChannelSerializer(data=request.data) if serializer.is_valid(): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) peer_pubkey = serializer.validated_data['peer_pubkey'] connected = False if Peers.objects.filter(pubkey=peer_pubkey, connected=True).exists(): @@ -2559,6 +2842,7 @@ def open_channel(request): return Response({'error': 'Invalid request!'}) @api_view(['POST']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def close_channel(request): serializer = CloseChannelSerializer(data=request.data) if serializer.is_valid(): @@ -2573,7 +2857,7 @@ def close_channel(request): channel_point.funding_txid_bytes = bytes.fromhex(funding_txid) channel_point.funding_txid_str = funding_txid channel_point.output_index = output_index - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) if serializer.validated_data['force']: for response in stub.CloseChannel(ln.CloseChannelRequest(channel_point=channel_point, force=True)): return Response({'message': 'Channel force closed! Closing TXID: ' + str(response.close_pending.txid[::-1].hex()) + ':' + str(response.close_pending.output_index)}) @@ -2592,11 +2876,12 @@ def close_channel(request): return Response({'error': 'Invalid request!'}) @api_view(['POST']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def add_invoice(request): serializer = AddInvoiceSerializer(data=request.data) if serializer.is_valid() and serializer.validated_data['value'] >= 0: try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) response = stub.AddInvoice(ln.Invoice(value=serializer.validated_data['value'])) return Response({'message': 'Invoice created!', 'data':str(response.payment_request)}) except Exception as e: @@ -2609,10 +2894,16 @@ def add_invoice(request): return Response({'error': 'Invalid request!'}) @api_view(['GET']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def new_address(request): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - response = stub.NewAddress(ln.NewAddressRequest(type=0)) + stub = lnrpc.LightningStub(lnd_connect()) + version = stub.GetInfo(ln.GetInfoRequest()).version + # Verify sufficient version to handle p2tr address creation + if float(version[:4]) >= 0.15: + response = stub.NewAddress(ln.NewAddressRequest(type=4)) + else: + response = stub.NewAddress(ln.NewAddressRequest(type=0)) return Response({'message': 'Retrieved new deposit address!', 'data':str(response.address)}) except Exception as e: error = str(e) @@ -2622,13 +2913,14 @@ def new_address(request): return Response({'error': 'Address creation failed! Error: ' + error_msg}) @api_view(['POST']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def update_alias(request): serializer = UpdateAliasSerializer(data=request.data) if serializer.is_valid(): peer_pubkey = serializer.validated_data['peer_pubkey'] if Channels.objects.filter(remote_pubkey=peer_pubkey).exists(): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) new_alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey)).node.alias update_channels = Channels.objects.filter(remote_pubkey=peer_pubkey) for channel in update_channels: @@ -2648,9 +2940,10 @@ def update_alias(request): return redirect('home') @api_view(['GET']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def get_info(request): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) response = stub.GetInfo(ln.GetInfoRequest()) target = {'identity_pubkey':response.identity_pubkey, 'alias':response.alias, 'num_active_channels':response.num_active_channels, 'num_peers':response.num_peers, 'block_height':response.block_height, 'block_hash':response.block_hash,'synced_to_chain':response.synced_to_chain,'testnet':response.testnet,'uris':[uri for uri in response.uris],'best_header_timestamp':response.best_header_timestamp,'version':response.version,'num_inactive_channels':response.num_inactive_channels,'chains':[{'chain':response.chains[i].chain,'network':response.chains[i].network} for i in range(0,len(response.chains))],'color':response.color,'synced_to_graph':response.synced_to_graph} return Response({'message': 'success', 'data':target}) @@ -2662,11 +2955,21 @@ def get_info(request): return Response({'error': 'Failed to call getinfo! Error: ' + error_msg}) @api_view(['GET']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def api_balances(request): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - response = stub.WalletBalance(ln.WalletBalanceRequest()) - target = {'total_balance':response.confirmed_balance, 'confirmed_balance':response.confirmed_balance} + stub = lnrpc.LightningStub(lnd_connect()) + balances = stub.WalletBalance(ln.WalletBalanceRequest()) + pending_channels = stub.PendingChannels(ln.PendingChannelsRequest()) + limbo_balance = pending_channels.total_limbo_balance + pending_open_balance = 0 + if pending_channels.pending_open_channels: + target_resp = pending_channels.pending_open_channels + for i in range(0,len(target_resp)): + pending_open_balance += target_resp[i].channel.local_balance + channels = Channels.objects.filter(is_open=1) + offchain_balance = channels.aggregate(Sum('local_balance'))['local_balance__sum'] + channels.aggregate(Sum('pending_outbound'))['pending_outbound__sum'] + pending_open_balance + limbo_balance + target = {'total_balance':(balances.total_balance + offchain_balance),'offchain_balance':offchain_balance,'onchain_balance':balances.total_balance, 'confirmed_balance':balances.confirmed_balance, 'unconfirmed_balance':balances.unconfirmed_balance} return Response({'message': 'success', 'data':target}) except Exception as e: error = str(e) @@ -2676,9 +2979,59 @@ def api_balances(request): return Response({'error': 'Failed to get wallet balances! Error: ' + error_msg}) @api_view(['GET']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) +def api_income(request): + try: + stub = lnrpc.LightningStub(lnd_connect()) + try: + days = int(request.GET.urlencode()[1:]) + except: + days = None + day_filter = datetime.now() - timedelta(days=days) if days else None + node_info = stub.GetInfo(ln.GetInfoRequest()) + payments = Payments.objects.filter(status=2).filter(creation_date__gte=day_filter) if day_filter else Payments.objects.filter(status=2) + onchain_txs = Onchain.objects.filter(time_stamp__gte=day_filter) if day_filter else Onchain.objects.all() + closures = Closures.objects.filter(close_height__gte=(node_info.block_height - (days*144))) if days else Closures.objects.all() + forwards = Forwards.objects.filter(forward_date__gte=day_filter) if day_filter else Forwards.objects.all() + forward_count = forwards.count() + forward_amount = 0 if forward_count == 0 else int(forwards.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000) + total_revenue = 0 if forward_count == 0 else int(forwards.aggregate(Sum('fee'))['fee__sum']) + invoices = Invoices.objects.filter(state=1, is_revenue=True).filter(settle_date__gte=day_filter) if day_filter else Invoices.objects.filter(state=1, is_revenue=True) + total_received = 0 if invoices.count() == 0 else int(invoices.aggregate(Sum('amt_paid'))['amt_paid__sum']) + total_revenue += total_received + total_revenue_ppm = 0 if forward_amount == 0 else int(total_revenue/(forward_amount/1000000)) + total_sent = 0 if payments.count() == 0 else int(payments.aggregate(Sum('value'))['value__sum']) + total_fees = 0 if payments.count() == 0 else int(payments.aggregate(Sum('fee'))['fee__sum']) + total_fees_ppm = 0 if total_sent == 0 else int(total_fees/(total_sent/1000000)) + onchain_costs = 0 if onchain_txs.count() == 0 else onchain_txs.aggregate(Sum('fee'))['fee__sum'] + close_fees = closures.aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures.exists() else 0 + onchain_costs += close_fees + profits = int(total_revenue-total_fees-onchain_costs) + target = { + 'forward_count': forward_count, + 'forward_amount': forward_amount, + 'total_revenue': total_revenue, + 'total_revenue_ppm': total_revenue_ppm, + 'total_fees': total_fees, + 'total_fees_ppm': total_fees_ppm, + 'onchain_costs': onchain_costs, + 'profits': profits, + 'profits_ppm': 0 if forward_amount == 0 else int(profits/(forward_amount/1000000)), + 'percent_cost': 0 if total_revenue == 0 else int(((total_fees+onchain_costs)/total_revenue)*100), + } + return Response({'message': 'success', 'data':target}) + except Exception as e: + error = str(e) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + return Response({'error': 'Failed to get revenue stats! Error: ' + error_msg}) + +@api_view(['GET']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) def pending_channels(request): try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) response = stub.PendingChannels(ln.PendingChannelsRequest()) if response.pending_open_channels or response.pending_closing_channels or response.pending_force_closing_channels or response.waiting_close_channels or response.total_limbo_balance: target = {} diff --git a/htlc_stream.py b/htlc_stream.py index 1a41044e..525d6b9b 100644 --- a/htlc_stream.py +++ b/htlc_stream.py @@ -2,7 +2,6 @@ from gui.lnd_deps import router_pb2 as lnr from gui.lnd_deps import router_pb2_grpc as lnrouter from gui.lnd_deps.lnd_connect import lnd_connect -from lndg import settings from os import environ from time import sleep environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings' @@ -11,7 +10,7 @@ def main(): try: - connection = lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER) + connection = lnd_connect() routerstub = lnrouter.RouterStub(connection) for response in routerstub.SubscribeHtlcEvents(lnr.SubscribeHtlcEventsRequest()): if response.event_type == 3 and str(response.link_fail_event) != '': @@ -26,7 +25,7 @@ def main(): amount = int(response.link_fail_event.info.outgoing_amt_msat/1000) wire_failure = response.link_fail_event.wire_failure failure_detail = response.link_fail_event.failure_detail - missed_fee = 0 if out_chan == None else round(((amount/1000000) * out_chan.local_fee_rate) + (out_chan.local_base_fee/1000), 3) + missed_fee = (response.link_fail_event.info.incoming_amt_msat - response.link_fail_event.info.outgoing_amt_msat)/1000 FailedHTLCs(amount=amount, chan_id_in=in_chan_id, chan_id_out=out_chan_id, chan_in_alias=in_chan_alias, chan_out_alias=out_chan_alias, chan_out_liq=out_chan_liq, chan_out_pending=out_chan_pending, wire_failure=wire_failure, failure_detail=failure_detail, missed_fee=missed_fee).save() except Exception as e: print('Error while running failed HTLC stream: ' + str(e)) diff --git a/initialize.py b/initialize.py index b2232b80..c8e433fa 100644 --- a/initialize.py +++ b/initialize.py @@ -5,7 +5,7 @@ from django.conf import settings BASE_DIR = Path(__file__).resolve().parent -def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenoise, debug, csrftrusted): +def write_settings(node_ip, lnd_tls_path, lnd_macaroon_path, lnd_database_path, lnd_network, lnd_rpc_server, whitenoise, debug, csrftrusted, nologinrequired): #Generate a unique secret to be used for your django site secret = secrets.token_urlsafe(64) if whitenoise: @@ -19,6 +19,14 @@ def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenois """ % (csrftrusted) else: csrf = '' + if not nologinrequired: + api_login = """ + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ],""" + else: + print('WARNING: No password login option detected, LNDg will not require authentication...') + api_login = '' settings_file = '''""" Django settings for lndg project. @@ -48,9 +56,12 @@ def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenois ALLOWED_HOSTS = ['%s'] %s -LND_DIR_PATH = '%s' +LND_TLS_PATH = '%s' +LND_MACAROON_PATH = '%s' +LND_DATABASE_PATH = '%s' LND_NETWORK = '%s' LND_RPC_SERVER = '%s' +LOGIN_REQUIRED = %s # Application definition @@ -129,10 +140,7 @@ def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenois REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - 'PAGE_SIZE': 100, - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', - ], + 'PAGE_SIZE': 100,%s 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', @@ -157,12 +165,12 @@ def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenois STATIC_URL = 'static/' STATIC_ROOT = os.path.join(BASE_DIR, 'gui/static/') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -''' % (secret, debug, node_ip, csrf, lnd_dir_path, lnd_network, lnd_rpc_server, wnl) +''' % (secret, debug, node_ip, csrf, lnd_tls_path, lnd_macaroon_path, lnd_database_path, lnd_network, lnd_rpc_server, not nologinrequired, wnl, api_login) try: f = open("lndg/settings.py", "x") f.close() except: - print('A settings file may already exist, please double check.') + print('A settings file may already exist, skipping creation...') return try: f = open("lndg/settings.py", "w") @@ -232,7 +240,7 @@ def write_supervisord_settings(sduser): f = open("/usr/local/etc/supervisord.conf", "x") f.close() except: - print('A supervisord settings file may already exist, please double check.') + print('A supervisord settings file may already exist, skipping creation...') return try: f = open("/usr/local/etc/supervisord.conf", "w") @@ -245,7 +253,7 @@ def main(): help_msg = "LNDg Initializer" parser = argparse.ArgumentParser(description = help_msg) parser.add_argument('-ip', '--nodeip',help = 'IP that will be used to access the LNDg page', default='*') - parser.add_argument('-dir', '--lnddir',help = 'LND Directory for tls cert and admin macaroon paths', default='~/.lnd') + parser.add_argument('-dir', '--lnddir',help = 'LND Directory for tls cert and admin macaroon paths', default=None) parser.add_argument('-net', '--network', help = 'Network LND will run over', default='mainnet') parser.add_argument('-server', '--rpcserver', help = 'Server address to use for rpc communications with LND', default='localhost:10009') parser.add_argument('-sd', '--supervisord', help = 'Setup supervisord to run jobs/rebalancer background processes', action='store_true') @@ -256,9 +264,16 @@ def main(): parser.add_argument('-u', '--adminuser', help = 'Setup a custom admin username', default='lndg-admin') parser.add_argument('-pw', '--adminpw', help = 'Setup a custom admin password', default=None) parser.add_argument('-csrf', '--csrftrusted', help = 'Set trusted CSRF origins', default=None) + parser.add_argument('-tls', '--tlscert', help = 'Set the path to the tls cert', default=None) + parser.add_argument('-mcrn', '--macaroon', help = 'Set the path to the macroon file', default=None) + parser.add_argument('-lnddb', '--lnddatabase', help = 'Set the path to the channel.db for monitoring', default=None) + parser.add_argument('-nologin', '--nologinrequired', help = 'By default, force all connections to be authenticated', action='store_true') args = parser.parse_args() + if args.lnddir and (args.tlscert or args.macaroon or args.lnddatabase): + parser.error("You may not use tlscert or macaroon flags with the lnddir flag") + exit node_ip = args.nodeip - lnd_dir_path = args.lnddir + lnd_dir_path = args.lnddir if args.lnddir else '~/.lnd' lnd_network = args.network lnd_rpc_server = args.rpcserver setup_supervisord = args.supervisord @@ -269,10 +284,14 @@ def main(): adminuser = args.adminuser adminpw = args.adminpw csrftrusted = args.csrftrusted + nologinrequired = args.nologinrequired + lnd_tls_path = args.tlscert if args.tlscert else lnd_dir_path + '/tls.cert' + lnd_macaroon_path = args.macaroon if args.macaroon else lnd_dir_path + '/data/chain/bitcoin/' + lnd_network + '/admin.macaroon' + lnd_database_path = args.lnddatabase if args.lnddatabase else lnd_dir_path + '/data/graph/' + lnd_network + '/channel.db' if docker: setup_supervisord = True whitenoise = True - write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenoise, debug, csrftrusted) + write_settings(node_ip, lnd_tls_path, lnd_macaroon_path, lnd_database_path, lnd_network, lnd_rpc_server, whitenoise, debug, csrftrusted, nologinrequired) if setup_supervisord: print('Supervisord setup requested...') write_supervisord_settings(sduser) diff --git a/jobs.py b/jobs.py index 92fcaf89..77b7ec88 100644 --- a/jobs.py +++ b/jobs.py @@ -1,5 +1,5 @@ import django -from django.db.models import Max, Min +from django.db.models import Max from datetime import datetime, timedelta from gui.lnd_deps import lightning_pb2 as ln from gui.lnd_deps import lightning_pb2_grpc as lnrpc @@ -12,15 +12,15 @@ from requests import get environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings' django.setup() -from gui.models import Payments, PaymentHops, Invoices, Forwards, Channels, Peers, Onchain, Closures, Resolutions, PendingHTLCs, LocalSettings, FailedHTLCs, Autofees, PendingChannels -from lndg.settings import LND_NETWORK +from gui.models import Payments, PaymentHops, Invoices, Forwards, Channels, Peers, Onchain, Closures, Resolutions, PendingHTLCs, LocalSettings, FailedHTLCs, Autofees, PendingChannels, Rebalancer def update_payments(stub): self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey inflight_payments = Payments.objects.filter(status=1).order_by('index') for payment in inflight_payments: payment_data = stub.ListPayments(ln.ListPaymentsRequest(include_incomplete=True, index_offset=payment.index-1, max_payments=1)).payments - if len(payment_data) > 0 and payment.payment_hash == payment_data[0].payment_hash: + #Ignore inflight payments before 30 days + if len(payment_data) > 0 and payment.payment_hash == payment_data[0].payment_hash and payment.creation_date > (datetime.now() - timedelta(days=30)): update_payment(stub, payment_data[0], self_pubkey) else: payment.status = 3 @@ -28,43 +28,14 @@ def update_payments(stub): last_index = Payments.objects.aggregate(Max('index'))['index__max'] if Payments.objects.exists() else 0 payments = stub.ListPayments(ln.ListPaymentsRequest(include_incomplete=True, index_offset=last_index, max_payments=100)).payments for payment in payments: + #print (f"{datetime.now().strftime('%c')} : Processing New {payment.payment_index=} {payment.status=} {payment.payment_hash=}") try: new_payment = Payments(creation_date=datetime.fromtimestamp(payment.creation_date), payment_hash=payment.payment_hash, value=round(payment.value_msat/1000, 3), fee=round(payment.fee_msat/1000, 3), status=payment.status, index=payment.payment_index) new_payment.save() - if payment.status == 2: - for attempt in payment.htlcs: - 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 - try: - alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=hop.pub_key, include_channels=False)).node.alias - except: - alias = '' - fee = hop.fee_msat/1000 - PaymentHops(payment_hash=new_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: - if new_payment.chan_out is None: - new_payment.chan_out = hop.chan_id - new_payment.chan_out_alias = alias - else: - new_payment.chan_out = 'MPP' - new_payment.chan_out_alias = 'MPP' - if hop_count == total_hops and 5482373484 in hop.custom_records and new_payment.keysend_preimage is None: - records = hop.custom_records - message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None - new_payment.keysend_preimage = records[5482373484].hex() - new_payment.message = message - if hop_count == total_hops and hop.pub_key == self_pubkey and new_payment.rebal_chan is None: - new_payment.rebal_chan = hop.chan_id - new_payment.save() - except: + except Exception as e: #Error inserting, try to update instead - update_payment(stub, payment, self_pubkey) + print (f"{datetime.now().strftime('%c')} : Error processing {new_payment=} : {str(e)=}") + update_payment(stub, payment, self_pubkey) def update_payment(stub, payment, self_pubkey): db_payment = Payments.objects.filter(payment_hash=payment.payment_hash)[0] @@ -73,11 +44,13 @@ def update_payment(stub, payment, self_pubkey): db_payment.fee = round(payment.fee_msat/1000, 3) db_payment.status = payment.status db_payment.index = payment.payment_index - db_payment.save() - if payment.status == 2: + if payment.status == 2 or payment.status == 1 or payment.status == 3: PaymentHops.objects.filter(payment_hash=db_payment).delete() + db_payment.chan_out = None + db_payment.rebal_chan = None + db_payment.save() for attempt in payment.htlcs: - if attempt.status == 1: + if attempt.status == 1 or attempt.status == 0 or attempt.status == 2: hops = attempt.route.hops hop_count = 0 cost_to = 0 @@ -89,9 +62,15 @@ def update_payment(stub, payment, self_pubkey): except: alias = '' 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() + if hop_count == total_hops: + # Add additional HTLC information in last hop alias + alias += f'[ {payment.status}-{attempt.status}-{attempt.failure.code}-{attempt.failure.failure_source_index} ]' + #if hop_count == total_hops: + #print (f"{datetime.now().strftime('%c')} : Debug Hop {attempt.attempt_id=} {attempt.route.total_amt=} {hop.mpp_record.payment_addr.hex()=} {hop.mpp_record.total_amt_msat=} {hop.amp_record=} {db_payment.payment_hash=}") + if attempt.status == 1 or attempt.status == 0 or (attempt.status == 2 and attempt.failure.code in (1,2,12)): + PaymentHops(payment_hash=db_payment, attempt_id=attempt.attempt_id, step=hop_count, chan_id=hop.chan_id, alias=alias, chan_capacity=hop.chan_capacity, node_pubkey=hop.pub_key, amt=round(hop.amt_to_forward_msat/1000, 3), fee=round(fee, 3), cost_to=round(cost_to, 3)).save() cost_to += fee - if hop_count == 1: + if hop_count == 1 and attempt.status == 1: if db_payment.chan_out is None: db_payment.chan_out = hop.chan_id db_payment.chan_out_alias = alias @@ -105,11 +84,74 @@ def update_payment(stub, payment, self_pubkey): db_payment.message = message if hop_count == total_hops and hop.pub_key == self_pubkey and db_payment.rebal_chan is None: db_payment.rebal_chan = hop.chan_id - db_payment.save() + db_payment.save() + try: + adjust_ar_amt( payment, db_payment.rebal_chan ) + except Exception as e: + print (f"{datetime.now().strftime('%c')} : Error adjusting AR Amount {payment=} {db_payment.rebal_chan=} : {str(e)=}") + +def adjust_ar_amt( payment, chan_id ): + if payment.status not in (2,3): + return + #skip rapid fire rebalances + last_rebalance_duration = Rebalancer.objects.filter(payment_hash=payment.payment_hash)[0].duration if Rebalancer.objects.filter(payment_hash=payment.payment_hash).exists() else 0 + #print (f"{datetime.now().strftime('%c')} : DEBUG {last_rebalance_duration=} {payment.payment_hash=}") + if last_rebalance_duration <= 1 or payment.status not in (2,3): + print (f"{datetime.now().strftime('%c')} : Skipping Liquidity Estimation {last_rebalance_duration=} {payment.payment_hash=}") + return + #To be coverted to settings later + lower_limit = 69420 + upper_limit = 2 + + if LocalSettings.objects.filter(key='AR-Target%').exists(): + ar_target = float(LocalSettings.objects.filter(key='AR-Target%')[0].value) + else: + LocalSettings(key='AR-Target%', value='5').save() + ar_target = 5 + + #Adjust AR Target Amount, increase if success reduce if failed. + db_channel = Channels.objects.filter(chan_id = chan_id)[0] if Channels.objects.filter(chan_id = chan_id).exists() else None + if payment.status == 2 and chan_id is not None: + if db_channel is not None and payment.value_msat/1000 > 1000 : + new_ar_amount = int(min(max(db_channel.ar_amt_target * 1.11, payment.value_msat/1000), db_channel.capacity*ar_target*upper_limit/100)) + if new_ar_amount > db_channel.ar_amt_target: + print (f"{datetime.now().strftime('%c')} : Increase AR Target Amount {chan_id=} {db_channel.alias=} {db_channel.ar_amt_target=} {new_ar_amount=}") + db_channel.ar_amt_target = new_ar_amount + db_channel.save() + + if payment.status == 3: + estimated_liquidity = 0 + attempt = None + for attempt in payment.htlcs: + total_hops=len(attempt.route.hops) + #Failure Codes https://github.com/lightningnetwork/lnd/blob/9f013f5058a7780075bca393acfa97aa0daec6a0/lnrpc/lightning.proto#L4200 + if (attempt.failure.code in (1,2) and attempt.failure.failure_source_index == total_hops) or attempt.failure.code == 12: + #Failure 1,2 from last hop indicating liquidity available, failure 12 shows fees in sufficient but liquidity available + estimated_liquidity += attempt.route.total_amt + chan_id=attempt.route.hops[len(attempt.route.hops)-1].chan_id + print (f"{datetime.now().strftime('%c')} : Liquidity Estimation {attempt.attempt_id=} {attempt.status=} {attempt.failure.code=} {chan_id=} {attempt.route.total_amt=} {payment.value_msat/1000=} {estimated_liquidity=} {payment.payment_hash=}") + + if estimated_liquidity == 0: + if attempt is not None: + #Could not estimate liquidity for valid attempts, reduce by half + estimated_liquidity = db_channel.ar_amt_target/2 if db_channel is not None else 0 + print (f"{datetime.now().strftime('%c')} : Liquidity Estimation not possible, halving {attempt.attempt_id=} {attempt.status=} {attempt.failure.code=} {chan_id=} {attempt.route.total_amt=} {payment.value_msat/1000=} {estimated_liquidity=} {payment.payment_hash=}") + else: + #Mostly a case of NO ROUTE + print (f"{datetime.now().strftime('%c')} : Liquidity Estimation not performed {payment.payment_hash=} {payment.status=} {chan_id=} {estimated_liquidity=} {attempt=}") + + if payment.value_msat/1000 >= lower_limit and estimated_liquidity <= payment.value_msat/1000 and estimated_liquidity > 0: + #Change AR amount. Ignore zero liquidity case which implies breakout from rapid fire AR + new_ar_amount = int(estimated_liquidity if estimated_liquidity > lower_limit else lower_limit) + if db_channel is not None and new_ar_amount < db_channel.ar_amt_target: + print (f"{datetime.now().strftime('%c')} : Decrease AR Target Amount {chan_id=} {db_channel.alias=} {db_channel.ar_amt_target=} {new_ar_amount=}") + db_channel.ar_amt_target = new_ar_amount + db_channel.save() def update_invoices(stub): open_invoices = Invoices.objects.filter(state=0).order_by('index') for open_invoice in open_invoices: + #print (f"{datetime.now().strftime('%c')} : Processing open invoice {open_invoice.index=} {open_invoice.state=} {open_invoice.r_hash=}") invoice_data = stub.ListInvoices(ln.ListInvoiceRequest(index_offset=open_invoice.index-1, num_max_invoices=1)).invoices if len(invoice_data) > 0 and open_invoice.r_hash == invoice_data[0].r_hash.hex(): update_invoice(stub, invoice_data[0], open_invoice) @@ -132,7 +174,7 @@ def update_invoice(stub, invoice, db_invoice): keysend_preimage = records[5482373484].hex() if 5482373484 in records else None message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None if 34349337 in records and 34349339 in records and 34349343 in records and 34349334 in records: - signerstub = lnsigner.SignerStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + signerstub = lnsigner.SignerStub(lnd_connect()) self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey try: valid = signerstub.VerifyMessage(lns.VerifyMessageReq(msg=(records[34349339]+bytes.fromhex(self_pubkey)+records[34349343]+records[34349334]), signature=records[34349337], pubkey=records[34349339])).valid @@ -207,33 +249,47 @@ def update_channels(stub): pending_channel = PendingChannels.objects.filter(funding_txid=txid, output_index=index)[0] if PendingChannels.objects.filter(funding_txid=txid, output_index=index).exists() else None try: chan_data = stub.GetChanInfo(ln.ChanInfoRequest(chan_id=channel.chan_id)) + old_fee_rate = db_channel.local_fee_rate if db_channel.local_fee_rate is not None else 0 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.local_disabled = chan_data.node2_policy.disabled db_channel.local_cltv = chan_data.node2_policy.time_lock_delta + db_channel.local_min_htlc_msat = chan_data.node2_policy.min_htlc + db_channel.local_max_htlc_msat = chan_data.node2_policy.max_htlc_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 db_channel.remote_disabled = chan_data.node1_policy.disabled db_channel.remote_cltv = chan_data.node1_policy.time_lock_delta + db_channel.remote_min_htlc_msat = chan_data.node1_policy.min_htlc + db_channel.remote_max_htlc_msat = chan_data.node1_policy.max_htlc_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.local_disabled = chan_data.node1_policy.disabled db_channel.local_cltv = chan_data.node1_policy.time_lock_delta + db_channel.local_min_htlc_msat = chan_data.node1_policy.min_htlc + db_channel.local_max_htlc_msat = chan_data.node1_policy.max_htlc_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 db_channel.remote_disabled = chan_data.node2_policy.disabled db_channel.remote_cltv = chan_data.node2_policy.time_lock_delta + db_channel.remote_min_htlc_msat = chan_data.node2_policy.min_htlc + db_channel.remote_max_htlc_msat = chan_data.node2_policy.max_htlc_msat except: + old_fee_rate = 0 db_channel.local_base_fee = 0 db_channel.local_fee_rate = 0 db_channel.local_disabled = False db_channel.local_cltv = 40 + db_channel.local_min_htlc_msat = 0 + db_channel.local_max_htlc_msat = 0 db_channel.remote_base_fee = 0 db_channel.remote_fee_rate = 0 db_channel.remote_disabled = False db_channel.remote_cltv = 40 + db_channel.remote_min_htlc_msat = 0 + db_channel.remote_max_htlc_msat = 0 db_channel.local_balance = channel.local_balance db_channel.remote_balance = channel.remote_balance db_channel.unsettled_balance = channel.unsettled_balance @@ -285,7 +341,7 @@ def update_channels(stub): db_channel.local_fee_rate = fee_rate db_channel.local_cltv = cltv db_channel.fees_updated = datetime.now() - if pending_channel.auto_rebalance: + if pending_channel.auto_rebalance is not None: db_channel.auto_rebalance = pending_channel.auto_rebalance if pending_channel.ar_amt_target: db_channel.ar_amt_target = pending_channel.ar_amt_target @@ -295,10 +351,15 @@ def update_channels(stub): db_channel.ar_out_target = pending_channel.ar_out_target if pending_channel.ar_max_cost: db_channel.ar_max_cost = pending_channel.ar_max_cost - if pending_channel.auto_fees: + if pending_channel.auto_fees is not None: db_channel.auto_fees = pending_channel.auto_fees pending_channel.delete() db_channel.save() + if db_channel.local_fee_rate != old_fee_rate: + print (f"{datetime.now().strftime('%c')} : Ext Fee Change Detected {db_channel.chan_id=} {db_channel.alias=} {old_fee_rate=} {db_channel.local_fee_rate=}") + #External Fee change detected, update auto fee log + Autofees(chan_id=db_channel.chan_id, peer_alias=db_channel.alias, setting=(f"Ext"), old_value=old_fee_rate, new_value=db_channel.local_fee_rate).save() + counter += 1 chan_list.append(channel.chan_id) records = Channels.objects.filter(is_open=True).count() @@ -361,12 +422,12 @@ def network_links(): return network_links def get_tx_fees(txid): - base_url = network_links() + ('/testnet' if LND_NETWORK == 'testnet' else '') + '/api/tx/' + base_url = network_links() + ('/testnet' if settings.LND_NETWORK == 'testnet' else '') + '/api/tx/' try: request_data = get(base_url + txid).json() fee = request_data['fee'] except Exception as e: - print('Error getting closure fees for ', txid, ':', str(e)) + print(f"{datetime.now().strftime('%c')} : Error getting closure fees {txid=} {str(e)=}") fee = 0 return fee @@ -381,7 +442,7 @@ def update_closures(stub): channel = Channels.objects.filter(chan_id=closure.chan_id)[0] if Channels.objects.filter(chan_id=closure.chan_id).exists() else None resolution_count = len(closure.resolutions) txid, index = closure.channel_point.split(':') - closing_costs = get_tx_fees(closure.closing_tx_hash) if closure.open_initiator == 1 else 0 + closing_costs = get_tx_fees(closure.closing_tx_hash) if (closure.open_initiator != 2 and closure.close_type not in [4, 5]) else 0 db_closure = Closures(chan_id=closure.chan_id, funding_txid=txid, funding_index=index, closing_tx=closure.closing_tx_hash, remote_pubkey=closure.remote_pubkey, capacity=closure.capacity, close_height=closure.close_height, settled_balance=closure.settled_balance, time_locked_balance=closure.time_locked_balance, close_type=closure.close_type, open_initiator=closure.open_initiator, close_initiator=closure.close_initiator, resolution_count=resolution_count) try: db_closure.save() @@ -395,9 +456,8 @@ def update_closures(stub): if resolution.resolution_type != 2: closing_costs += get_tx_fees(resolution.sweep_txid) Resolutions(chan_id=closure.chan_id, resolution_type=resolution.resolution_type, outcome=resolution.outcome, outpoint_tx=resolution.outpoint.txid_str, outpoint_index=resolution.outpoint.output_index, amount_sat=resolution.amount_sat, sweep_txid=resolution.sweep_txid).save() - if channel: - channel.closing_costs = closing_costs - channel.save() + db_closure.closing_costs = closing_costs + db_closure.save() def reconnect_peers(stub): inactive_peers = Channels.objects.filter(is_open=True, is_active=False, private=False).values_list('remote_pubkey', flat=True).distinct() @@ -410,20 +470,29 @@ def reconnect_peers(stub): print (f"{datetime.now().strftime('%c')} : Reconnecting {peer.alias=} {peer.pubkey=} {peer.last_reconnected=}") if peer.connected == True: print (f"{datetime.now().strftime('%c')} : ... Inactive channel is still connected to peer, disconnecting peer. {peer.alias=} {inactive_peer=}") - stub.DisconnectPeer(ln.DisconnectPeerRequest(pub_key=inactive_peer)) - peer.connected = False - peer.save() + try: + response = stub.DisconnectPeer(ln.DisconnectPeerRequest(pub_key=inactive_peer)) + print (f"{datetime.now().strftime('%c')} : .... Status Disconnect {peer.alias=} {inactive_peer=} {response=}") + peer.connected = False + peer.save() + except Exception as e: + print (f"{datetime.now().strftime('%c')} : .... Error disconnecting {peer.alias} {inactive_peer=} {str(e)=}") + try: node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=inactive_peer, include_channels=False)).node host = node.addresses[0].addr - except: - print (f"{datetime.now().strftime('%c')} : ... Unable to find node info on graph, using last known value {peer.alias=} {peer.pubkey=} {peer.address=}") + except Exception as e: + print (f"{datetime.now().strftime('%c')} : ... Unable to find node info on graph, using last known value {peer.alias=} {peer.pubkey=} {peer.address=} {str(e)=}") host = peer.address - address = ln.LightningAddress(pubkey=inactive_peer, host=host) + #address = ln.LightningAddress(pubkey=inactive_peer, host=host) print (f"{datetime.now().strftime('%c')} : ... Attempting connection to {peer.alias=} {inactive_peer=} {host=}") try: - response = stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=address, perm=False, timeout=5)) - print (f"{datetime.now().strftime('%c')} : .... Status {peer.alias=} {inactive_peer=} {response=}") + #try both the graph value and last know value + stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=ln.LightningAddress(pubkey=inactive_peer, host=host), perm=True, timeout=5)) + if host != peer.address and peer.address[:9] != '127.0.0.1': + stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=ln.LightningAddress(pubkey=inactive_peer, host=peer.address), perm=True, timeout=5)) + #response = stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=address, perm=False, timeout=5)) + #print (f"{datetime.now().strftime('%c')} : .... Status {peer.alias=} {inactive_peer=} {response=}") except Exception as e: error = str(e) details_index = error.find('details =') + 11 @@ -462,7 +531,7 @@ def clean_payments(stub): finally: payment.cleaned = True payment.save() - print (f"{datetime.now().strftime('%c')} : Cleaned {payment.index=} {payment.status=} {payment.cleaned=} {payment.payment_hash=}") + #print (f"{datetime.now().strftime('%c')} : Cleaned {payment.index=} {payment.status=} {payment.cleaned=} {payment.payment_hash=}") def auto_fees(stub): if LocalSettings.objects.filter(key='AF-Enabled').exists(): @@ -501,7 +570,12 @@ def auto_fees(stub): else: LocalSettings(key='AF-FailedHTLCs', value='25').save() failed_htlc_limit = 25 - channels_df['eligible'] = channels_df.apply(lambda row: (datetime.now()-row['fees_updated']).total_seconds() > 86400, axis=1) + if LocalSettings.objects.filter(key='AF-UpdateHours').exists(): + update_hours = int(LocalSettings.objects.filter(key='AF-UpdateHours')[0].value) + else: + LocalSettings(key='AF-UpdateHours', value='24').save() + update_hours = 24 + channels_df['eligible'] = channels_df.apply(lambda row: (datetime.now()-row['fees_updated']).total_seconds() > (update_hours*3600), axis=1) channels_df = channels_df[channels_df['eligible']==True] if channels_df.shape[0] > 0: failed_htlc_df = DataFrame.from_records(FailedHTLCs.objects.filter(timestamp__gte=filter_1day).order_by('-id').values()) @@ -565,11 +639,12 @@ def auto_fees(stub): channel.local_fee_rate = target_channel['new_rate'] channel.fees_updated = datetime.now() channel.save() - Autofees(chan_id=channel.chan_id, peer_alias=channel.alias, setting='Fee Rate', old_value=target_channel['local_fee_rate'], new_value=target_channel['new_rate']).save() + Autofees(chan_id=channel.chan_id, peer_alias=channel.alias, setting=(f"AF [ {target_channel['net_routed_7day']}:{target_channel['in_percent']}:{target_channel['out_percent']} ]"), old_value=target_channel['local_fee_rate'], new_value=target_channel['new_rate']).save() def main(): + #print (f"{datetime.now().strftime('%c')} : Entering Jobs") try: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) #Update data update_peers(stub) update_channels(stub) @@ -582,7 +657,7 @@ def main(): clean_payments(stub) auto_fees(stub) except Exception as e: - print('Error processing background data: ' + str(e)) - + print (f"{datetime.now().strftime('%c')} : Error processing background data: {str(e)=}") + #print (f"{datetime.now().strftime('%c')} : Exit Jobs") if __name__ == '__main__': main() diff --git a/keysend.py b/keysend.py index defc22ef..febc9aeb 100644 --- a/keysend.py +++ b/keysend.py @@ -1,6 +1,5 @@ import secrets, time, struct from hashlib import sha256 -from lndg import settings 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 @@ -12,7 +11,7 @@ def keysend(target_pubkey, msg, amount, fee_limit, timeout, sign): #Construct and send try: - routerstub = lnrouter.RouterStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + routerstub = lnrouter.RouterStub(lnd_connect()) secret = secrets.token_bytes(32) hashed_secret = sha256(secret).hexdigest() custom_records = [(5482373484, secret),] @@ -20,8 +19,8 @@ def keysend(target_pubkey, msg, amount, fee_limit, timeout, sign): if len(msg) > 0: custom_records.append((34349334, bytes.fromhex(msg.encode('utf-8').hex()))) if sign == True: - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - signerstub = lnsigner.SignerStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + stub = lnrpc.LightningStub(lnd_connect()) + signerstub = lnsigner.SignerStub(lnd_connect()) self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey timestamp = struct.pack(">i", int(time.time())) signature = signerstub.SignMessage(lns.SignMessageReq(msg=(bytes.fromhex(self_pubkey)+bytes.fromhex(target_pubkey)+timestamp+bytes.fromhex(msg.encode('utf-8').hex())), key_loc=lns.KeyLocator(key_family=6, key_index=0))).signature diff --git a/lndg/urls.py b/lndg/urls.py index e3b8a13a..77164087 100644 --- a/lndg/urls.py +++ b/lndg/urls.py @@ -13,9 +13,11 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.contrib import admin from django.urls import path, include +from django.views.generic.base import RedirectView +from django.contrib.staticfiles.storage import staticfiles_storage urlpatterns = [ + path('favicon.ico', RedirectView.as_view(url=staticfiles_storage.url("favicon.ico"))), path('', include('gui.urls')), -] +] \ No newline at end of file diff --git a/nginx.sh b/nginx.sh index cf158be2..100410bf 100644 --- a/nginx.sh +++ b/nginx.sh @@ -87,7 +87,8 @@ EOF function setup_nginx() { cat << EOF > /etc/nginx/sites-enabled/lndg -user $INSTALL_USER +# the below setting can sometimes help resolve permission issues +# user $INSTALL_USER upstream django { server unix://$HOME_DIR/lndg/lndg.sock; # for a file socket diff --git a/rebalancer.py b/rebalancer.py index 3648fc11..1da49980 100644 --- a/rebalancer.py +++ b/rebalancer.py @@ -6,7 +6,6 @@ from gui.lnd_deps import router_pb2 as lnr from gui.lnd_deps import router_pb2_grpc as lnrouter from gui.lnd_deps.lnd_connect import lnd_connect -from lndg import settings from os import environ environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings' django.setup() @@ -29,7 +28,7 @@ def run_rebalancer(rebalance): rebalance.start = datetime.now() try: #Open connection with lnd via grpc - connection = lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER) + connection = lnd_connect() stub = lnrpc.LightningStub(connection) routerstub = lnrouter.RouterStub(connection) chan_ids = json.loads(rebalance.outgoing_chan_ids) @@ -78,14 +77,27 @@ def run_rebalancer(rebalance): finally: rebalance.stop = datetime.now() rebalance.save() - if rebalance.status == 2: + original_alias = rebalance.target_alias + inc=1.21 + dec=2 + if rebalance.status ==2: update_channels(stub, rebalance.last_hop_pubkey, successful_out) auto_rebalance_channels = Channels.objects.filter(is_active=True, is_open=True, private=False).annotate(percent_outbound=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).annotate(inbound_can=(((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity'))/Sum('ar_in_target')) inbound_cans = auto_rebalance_channels.filter(remote_pubkey=rebalance.last_hop_pubkey).filter(auto_rebalance=True, inbound_can__gte=1) - outbound_cans = list(auto_rebalance_channels.filter(auto_rebalance=False, percent_outbound__gte=F('ar_out_target')).values_list('chan_id', flat=True)) + outbound_cans = list(auto_rebalance_channels.filter(auto_rebalance=False, percent_outbound__gte=F('ar_out_target')).exclude(remote_pubkey=rebalance.last_hop_pubkey).values_list('chan_id', flat=True)) if len(inbound_cans) > 0 and len(outbound_cans) > 0: - next_rebalance = Rebalancer(value=rebalance.value, fee_limit=rebalance.fee_limit, outgoing_chan_ids=str(outbound_cans).replace('\'', ''), last_hop_pubkey=rebalance.last_hop_pubkey, target_alias=rebalance.target_alias, duration=1) + next_rebalance = Rebalancer(value=int(rebalance.value*inc), fee_limit=round(rebalance.fee_limit*inc, 3), outgoing_chan_ids=str(outbound_cans).replace('\'', ''), last_hop_pubkey=rebalance.last_hop_pubkey, target_alias=original_alias, duration=1) next_rebalance.save() + print (f"{datetime.now().strftime('%c')} : RapidFire up {next_rebalance.target_alias=} {next_rebalance.value=} {rebalance.value=}") + else: + next_rebalance = None + elif rebalance.status > 2 and rebalance.duration <= 1 and rebalance.value > 69420: + #Previous Rapidfire with increased value failed, try with lower value up to 69420. + inbound_cans = auto_rebalance_channels.filter(remote_pubkey=rebalance.last_hop_pubkey).filter(auto_rebalance=True, inbound_can__gte=1) + if len(inbound_cans) > 0 and len(outbound_cans) > 0: + next_rebalance = Rebalancer(value=int(rebalance.value/dec), fee_limit=round(rebalance.fee_limit/dec, 3), outgoing_chan_ids=str(outbound_cans).replace('\'', ''), last_hop_pubkey=rebalance.last_hop_pubkey, target_alias=original_alias, duration=1) + next_rebalance.save() + print (f"{datetime.now().strftime('%c')} : RapidFire Down {next_rebalance.target_alias=} {next_rebalance.value=} {rebalance.value=}") else: next_rebalance = None else: @@ -121,7 +133,7 @@ def auto_schedule(): if not LocalSettings.objects.filter(key='AR-Inbound%').exists(): LocalSettings(key='AR-Inbound%', value='100').save() 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) + inbound_cans = auto_rebalance_channels.filter(auto_rebalance=True, inbound_can__gte=1).order_by('-remote_balance') if len(inbound_cans) > 0 and len(outbound_cans) > 0: if LocalSettings.objects.filter(key='AR-MaxFeeRate').exists(): max_fee_rate = int(LocalSettings.objects.filter(key='AR-MaxFeeRate')[0].value) @@ -194,7 +206,11 @@ def auto_enable(): for peer_channel in lookup_channels.filter(chan_id__in=chan_list): #print('Processing: ', peer_channel.alias, ' : ', peer_channel.chan_id, ' : ', oapD, " : ", iapD, ' : ', outbound_percent, ' : ', inbound_percent) - if oapD > (iapD*1.10) and outbound_percent > 75: + if peer_channel.ar_out_target == 100 and peer_channel.auto_rebalance == True: + #Special Case for LOOP, Wos, etc. Always Auto Rebalance if enabled to keep outbound full. + print (f"{datetime.now().strftime('%c')} : Pass {peer_channel.alias=} {peer_channel.chan_id=} {peer_channel.ar_out_target=} {peer_channel.auto_rebalance=}") + pass + elif oapD > (iapD*1.10) and outbound_percent > 75: #print('Case 1: Pass') pass elif oapD > (iapD*1.10) and inbound_percent > 75 and peer_channel.auto_rebalance == False: diff --git a/systemd.md b/systemd.md index fa89ebca..b8f2dc2b 100644 --- a/systemd.md +++ b/systemd.md @@ -93,7 +93,7 @@ User= Group= ExecStart=/usr/bin/bash /home//lndg/htlc_stream.sh StandardError=append:/var/log/lnd_htlc_stream_error.log -Restart=on-failure +Restart=always RestartSec=60s [Install] WantedBy=multi-user.target diff --git a/systemd.sh b/systemd.sh index a8fd6a9e..51c0da51 100644 --- a/systemd.sh +++ b/systemd.sh @@ -83,7 +83,7 @@ User=$INSTALL_USER Group=$INSTALL_USER ExecStart=/usr/bin/bash $HOME_DIR/lndg/htlc_stream.sh StandardError=append:/var/log/lnd_htlc_stream_error.log -Restart=on-failure +Restart=always RestartSec=60s [Install] WantedBy=multi-user.target