From c63c065c6c98a5ffa24bbb763814aab57944d5b1 Mon Sep 17 00:00:00 2001 From: cryptosharks131 <38626122+cryptosharks131@users.noreply.github.com> Date: Fri, 26 Aug 2022 20:56:46 -0400 Subject: [PATCH] v1.3.0 (#120) --- .github/workflows/on-push-github.yml | 46 ++ .github/workflows/on-tag-github.yml | 47 ++ README.md | 45 +- gui/forms.py | 12 +- gui/migrations/0030_auto_20220722_0912.py | 70 +++ gui/migrations/0031_pendingchannels.py | 33 ++ gui/models.py | 38 +- gui/templates/advanced.html | 6 +- gui/templates/base.html | 2 +- gui/templates/channel.html | 30 +- gui/templates/closures.html | 116 ++++- gui/templates/failed_htlcs.html | 6 +- gui/templates/home.html | 180 ++++++-- gui/templates/income.html | 74 +++ gui/templates/invoices.html | 10 +- gui/templates/open_list.html | 4 +- gui/templates/payments.html | 8 +- gui/templates/pending_htlcs.html | 10 +- gui/templates/rebalances.html | 45 ++ gui/templates/rebalancing.html | 22 +- gui/templates/resolutions.html | 2 +- gui/templates/route.html | 8 +- gui/urls.py | 6 +- gui/views.py | 528 +++++++++++++++++++--- initialize.py | 22 +- jobs.py | 255 +++++++---- nginx.sh | 2 + rebalancer.py | 82 ++-- requirements.txt | 3 +- 29 files changed, 1382 insertions(+), 330 deletions(-) create mode 100644 .github/workflows/on-push-github.yml create mode 100644 .github/workflows/on-tag-github.yml create mode 100644 gui/migrations/0030_auto_20220722_0912.py create mode 100644 gui/migrations/0031_pendingchannels.py create mode 100644 gui/templates/income.html create mode 100644 gui/templates/rebalances.html diff --git a/.github/workflows/on-push-github.yml b/.github/workflows/on-push-github.yml new file mode 100644 index 0000000..88913e4 --- /dev/null +++ b/.github/workflows/on-push-github.yml @@ -0,0 +1,46 @@ +name: Build on push + +permissions: + packages: write + +on: + push: + branches: + - master + +jobs: + build: + name: Build image + runs-on: ubuntu-22.04 + + steps: + - name: Checkout project + uses: actions/checkout@v3 + + - name: Set env variables + run: | + echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV + IMAGE_NAME="${GITHUB_REPOSITORY#*/}" + echo "IMAGE_NAME=${IMAGE_NAME//docker-/}" >> $GITHUB_ENV + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + id: qemu + + - name: Setup Docker buildx action + uses: docker/setup-buildx-action@v2 + id: buildx + + - name: Run Docker buildx + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:latest \ + --output "type=registry" ./ diff --git a/.github/workflows/on-tag-github.yml b/.github/workflows/on-tag-github.yml new file mode 100644 index 0000000..e27e887 --- /dev/null +++ b/.github/workflows/on-tag-github.yml @@ -0,0 +1,47 @@ +name: Build on tag + +permissions: + packages: write + +on: + push: + tags: + - v[0-9]+.[0-9]+.[0-9]+ + - v[0-9]+.[0-9]+.[0-9]+-* + +jobs: + build: + name: Build image + runs-on: ubuntu-22.04 + + steps: + - name: Checkout project + uses: actions/checkout@v3 + + - name: Set env variables + run: | + echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV + IMAGE_NAME="${GITHUB_REPOSITORY#*/}" + echo "IMAGE_NAME=${IMAGE_NAME//docker-/}" >> $GITHUB_ENV + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + id: qemu + + - name: Setup Docker buildx action + uses: docker/setup-buildx-action@v2 + id: buildx + + - name: Run Docker buildx + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:$TAG \ + --output "type=registry" ./ diff --git a/README.md b/README.md index c119099..c797789 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # LNDg Lite GUI web interface to analyze lnd data and manage your node with automation. -Start by choosing one of the following installation methods: -[Docker Installation](https://github.com/cryptosharks131/lndg#docker-installation-requires-docker-and-docker-compose-be-installed) | [Umbrel Installation](https://github.com/cryptosharks131/lndg#umbrel-installation) | [Manual Installation](https://github.com/cryptosharks131/lndg#manual-installation) +Start by choosing one of the following installation methods: [Docker Installation](https://github.com/cryptosharks131/lndg#docker-installation-requires-docker-and-docker-compose-be-installed) | [Manual Installation](https://github.com/cryptosharks131/lndg#manual-installation) + +LNDg can also be found directly on popular apps like Umbrel and Citadel with a 1-click install from the GUI. ## Docker Installation (requires docker and docker-compose be installed) ### Build and deploy @@ -37,46 +38,6 @@ docker-compose up -d docker system prune -f ``` -## Manual Umbrel Installation (now available directly from the Umbrel app store) - -### Build and deploy -1. Log into your umbrel via ssh -2. Clone respository `git clone https://github.com/cryptosharks131/lndg.git` -3. Change directory `cd lndg` -4. Copy and replace the contents of the `docker-compose.yaml` with the below: `nano docker-compose.yaml` -``` -services: - lndg: - build: . - volumes: - - /home/umbrel/umbrel/lnd:/root/.lnd:ro - - /home/umbrel/lndg/data:/lndg/data:rw - command: - - sh - - -c - - python initialize.py -net 'mainnet' -server '10.21.21.9:10009' -d && supervisord && python manage.py runserver 0.0.0.0:8000 - ports: - - 8889:8000 -networks: - default: - external: true - name: umbrel_main_network -``` -5. Deploy your docker image: `docker-compose up -d` -6. You can now access LNDg via your browser on port 8889: `http://umbrel.local:8889` -7. Open and copy the password from output file: `nano data/lndg-admin.txt` -8. Use the password from the output file and the username `lndg-admin` to login - -### Updating -``` -docker-compose down -docker-compose build --no-cache -docker-compose up -d - -# OPTIONAL: remove unused builds and objects -docker system prune -f -``` - ## Manual Installation ### Step 1 - Install lndg 1. Clone respository `git clone https://github.com/cryptosharks131/lndg.git` diff --git a/gui/forms.py b/gui/forms.py index a22e2a5..4b18842 100644 --- a/gui/forms.py +++ b/gui/forms.py @@ -80,10 +80,13 @@ class AutoRebalanceForm(forms.Form): target_time = forms.IntegerField(label='target_time', required=False) fee_rate = forms.IntegerField(label='fee_rate', required=False) outbound_percent = forms.FloatField(label='outbound_percent', required=False) + inbound_percent = forms.FloatField(label='inbound_percent', required=False) max_cost = forms.FloatField(label='max_cost', required=False) variance = forms.IntegerField(label='variance', required=False) wait_period = forms.IntegerField(label='wait_period', required=False) autopilot = forms.IntegerField(label='autopilot', required=False) + autopilotdays = forms.IntegerField(label='autopilotdays', required=False) + targetallchannels = forms.BooleanField(widget=forms.CheckboxSelectMultiple, required=False) updates_channel_codes = [ (0, 'base_fee'), @@ -96,6 +99,7 @@ updates_channel_codes = [ (7, 'channel_state'), (8, 'auto_fees'), (9, 'cltv'), + (10, 'closing_costs'), ] class UpdateChannel(forms.Form): @@ -103,6 +107,12 @@ class UpdateChannel(forms.Form): target = forms.IntegerField(label='target') update_target = forms.ChoiceField(label='update_target', choices=updates_channel_codes) +class UpdatePending(forms.Form): + funding_txid = forms.CharField(label='funding_txid', max_length=64) + output_index = forms.IntegerField(label='output_index') + target = forms.IntegerField(label='target') + update_target = forms.ChoiceField(label='update_target', choices=updates_channel_codes) + class UpdateSetting(forms.Form): key = forms.CharField(label='setting', max_length=20) value = forms.CharField(label='value', max_length=50) @@ -128,4 +138,4 @@ class BatchOpenForm(forms.Form): amt9 = forms.IntegerField(label='amt9', required=False) pubkey10 = forms.CharField(label='pubkey10', max_length=66, required=False) amt10 = forms.IntegerField(label='amt10', required=False) - fee_rate = forms.IntegerField(label='fee_rate') \ No newline at end of file + fee_rate = forms.IntegerField(label='fee_rate') diff --git a/gui/migrations/0030_auto_20220722_0912.py b/gui/migrations/0030_auto_20220722_0912.py new file mode 100644 index 0000000..4833954 --- /dev/null +++ b/gui/migrations/0030_auto_20220722_0912.py @@ -0,0 +1,70 @@ +# Generated by Django 3.2.7 on 2022-07-21 05:57 + +from django.db import migrations, models +from requests import get +from lndg.settings import LND_NETWORK + +def update_close_fees(apps, schedma_editor): + channels = apps.get_model('gui', 'channels') + closures = apps.get_model('gui', 'closures') + resolutions = apps.get_model('gui', 'resolutions') + settings = apps.get_model('gui', 'localsettings') + def network_links(): + if settings.objects.filter(key='GUI-NetLinks').exists(): + network_links = str(settings.objects.filter(key='GUI-NetLinks')[0].value) + else: + network_links = 'https://mempool.space' + return network_links + def get_tx_fees(txid): + base_url = network_links() + ('/testnet' if 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)) + fee = 0 + return fee + try: + for closure in closures.objects.exclude(open_initiator=2, close_type=0): + if channels.objects.filter(chan_id=closure.chan_id).exists(): + channel = channels.objects.filter(chan_id=closure.chan_id)[0] + closing_costs = get_tx_fees(closure.closing_tx) if closure.open_initiator == 1 else 0 + for resolution in resolutions.objects.filter(chan_id=closure.chan_id).exclude(resolution_type=2): + closing_costs += get_tx_fees(resolution.sweep_txid) + channel.closing_costs = closing_costs + channel.save() + except Exception as e: + print('Migration step failed:', str(e)) + +def revert_close_fees(apps, schedma_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0029_update_percent_vars'), + ] + + operations = [ + migrations.AddField( + model_name='channels', + name='closing_costs', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='rebalancer', + name='fees_paid', + field=models.FloatField(default=None, null=True), + ), + migrations.AlterField( + model_name='channels', + name='ar_in_target', + field=models.IntegerField(), + ), + migrations.AlterField( + model_name='channels', + name='auto_fees', + field=models.BooleanField(), + ), + migrations.RunPython(update_close_fees, revert_close_fees), + ] diff --git a/gui/migrations/0031_pendingchannels.py b/gui/migrations/0031_pendingchannels.py new file mode 100644 index 0000000..415d7fb --- /dev/null +++ b/gui/migrations/0031_pendingchannels.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.7 on 2022-08-05 11:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0030_auto_20220722_0912'), + ] + + operations = [ + migrations.CreateModel( + name='PendingChannels', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('funding_txid', models.CharField(max_length=64)), + ('output_index', models.IntegerField()), + ('local_base_fee', models.IntegerField(default=None, null=True)), + ('local_fee_rate', models.IntegerField(default=None, null=True)), + ('local_cltv', models.IntegerField(default=None, null=True)), + ('auto_rebalance', models.BooleanField(default=None, null=True)), + ('ar_amt_target', models.BigIntegerField(default=None, null=True)), + ('ar_in_target', models.IntegerField(default=None, null=True)), + ('ar_out_target', models.IntegerField(default=None, null=True)), + ('ar_max_cost', models.IntegerField(default=None, null=True)), + ('auto_fees', models.BooleanField(default=None, null=True)), + ], + options={ + 'unique_together': {('funding_txid', 'output_index')}, + }, + ), + ] diff --git a/gui/models.py b/gui/models.py index 89a7bc1..1011dd7 100644 --- a/gui/models.py +++ b/gui/models.py @@ -95,13 +95,21 @@ class Channels(models.Model): last_update = models.DateTimeField() auto_rebalance = models.BooleanField(default=False) ar_amt_target = models.BigIntegerField() - ar_in_target = models.IntegerField(default=100) + ar_in_target = models.IntegerField() ar_out_target = models.IntegerField() ar_max_cost = models.IntegerField() fees_updated = models.DateTimeField(default=timezone.now) - auto_fees = models.BooleanField(default=False) + auto_fees = models.BooleanField() + closing_costs = models.IntegerField(default=0) def save(self, *args, **kwargs): + if self.auto_fees is None: + if LocalSettings.objects.filter(key='AF-Enabled').exists(): + enabled = int(LocalSettings.objects.filter(key='AF-Enabled')[0].value) + else: + LocalSettings(key='AF-Enabled', value='0').save() + enabled = 0 + self.auto_fees = False if enabled == 0 else True if not self.ar_out_target: if LocalSettings.objects.filter(key='AR-Outbound%').exists(): outbound_setting = int(LocalSettings.objects.filter(key='AR-Outbound%')[0].value) @@ -109,6 +117,13 @@ class Channels(models.Model): LocalSettings(key='AR-Outbound%', value='75').save() outbound_setting = 75 self.ar_out_target = outbound_setting + if not self.ar_in_target: + if LocalSettings.objects.filter(key='AR-Inbound%').exists(): + inbound_setting = int(LocalSettings.objects.filter(key='AR-Inbound%')[0].value) + else: + LocalSettings(key='AR-Inbound%', value='100').save() + inbound_setting = 100 + self.ar_in_target = inbound_setting if not self.ar_amt_target: if LocalSettings.objects.filter(key='AR-Target%').exists(): amt_setting = float(LocalSettings.objects.filter(key='AR-Target%')[0].value) @@ -153,6 +168,7 @@ class Rebalancer(models.Model): status = models.IntegerField(default=0) payment_hash = models.CharField(max_length=64, null=True, default=None) manual = models.BooleanField(default=False) + fees_paid = models.FloatField(null=True, default=None) class Meta: app_label = 'gui' @@ -247,4 +263,20 @@ class Autofees(models.Model): old_value = models.IntegerField() new_value = models.IntegerField() class Meta: - app_label = 'gui' \ No newline at end of file + app_label = 'gui' + +class PendingChannels(models.Model): + funding_txid = models.CharField(max_length=64) + output_index = models.IntegerField() + local_base_fee = models.IntegerField(null=True, default=None) + local_fee_rate = models.IntegerField(null=True, default=None) + local_cltv = models.IntegerField(null=True, default=None) + auto_rebalance = models.BooleanField(null=True, default=None) + ar_amt_target = models.BigIntegerField(null=True, default=None) + ar_in_target = models.IntegerField(null=True, default=None) + ar_out_target = models.IntegerField(null=True, default=None) + ar_max_cost = models.IntegerField(null=True, default=None) + 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 diff --git a/gui/templates/advanced.html b/gui/templates/advanced.html index ab4ea99..7b9eabe 100644 --- a/gui/templates/advanced.html +++ b/gui/templates/advanced.html @@ -204,11 +204,11 @@ {% 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'%} + {% 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 == 'AR-Time' %} - {% elif settings.key == 'AR-MaxFeeRate' or settings.key == 'AF-MaxRate' %} + {% elif settings.key == 'AR-MaxFeeRate' %} {% elif settings.key == 'AF-MaxRate' or settings.key == 'AF-MinRate' %} @@ -227,4 +227,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/base.html b/gui/templates/base.html index d9944eb..7e64a45 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -28,7 +28,7 @@ diff --git a/gui/templates/channel.html b/gui/templates/channel.html index 6485b3b..6260120 100644 --- a/gui/templates/channel.html +++ b/gui/templates/channel.html @@ -217,9 +217,9 @@ {{ forward.amt_out|intcomma }} {% if forward.chan_in_alias == '' %}---{% else %}{{ forward.chan_in_alias }}{% endif %} {% if forward.chan_out_alias == '' %}---{% else %}{{ forward.chan_out_alias }}{% endif %} - {{ forward.chan_id_in }} - {{ forward.chan_id_out }} - {{ forward.fee }} + {{ forward.chan_id_in }} + {{ forward.chan_id_out }} + {{ forward.fee|intcomma }} {{ forward.ppm|intcomma }} {% endfor %} @@ -235,21 +235,25 @@ Start Stop Scheduled Duration + Actual Duration Value Fee Limit Target PPM + Fees Paid Last Hop Alias Status {% for rebalance in rebalances %} {{ rebalance.requested|naturaltime }} - N/A{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %} - 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>N/A{% 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 == 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 %} @@ -281,9 +285,9 @@ {{ payment.fee|intcomma }} {{ payment.ppm|intcomma }} {% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %} - {% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %} - {% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %} - {% if payment.status == 2 %}Open{% else %}N/A{% endif %} + {% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}---{% endif %} + {% if payment.status == 2 %}{{ payment.chan_out }}{% else %}---{% endif %} + {% if payment.status == 2 %}Open{% else %}---{% endif %} {% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %} {% endfor %} @@ -308,13 +312,13 @@ {% for invoice in invoices %} {{ invoice.creation_date|naturaltime }} - {% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}N/A{% endif %} + {% 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 %}N/A{% endif %} + {% 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 %}N/A{% endif %} - {% if invoice.state == 1 and invoice.chan_in != None %}{{ invoice.chan_in }}{% else %}N/A{% 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 %} {% endfor %} @@ -362,4 +366,4 @@

No data found for this channel!

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/closures.html b/gui/templates/closures.html index 402d83b..dce4c67 100644 --- a/gui/templates/closures.html +++ b/gui/templates/closures.html @@ -2,6 +2,105 @@ {% block title %} {{ block.super }} - Closures{% endblock %} {% block content %} {% load humanize %} +{% if pending_closed %} +
+

Pending Close Channels

+ + + + + + + + + + + + + {% for channel in pending_closed %} + + + + {% with funding_txid=channel.channel_point|slice:":-2" %} + + {% endwith %} + + + + + + + + {% endfor %} +
Channel IDPeer AliasChannel PointCapacityLocal BalanceRemote BalanceBalance In LimboLocal Commit FeesClosing TX
{{ channel.chan_id }}{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}{{ channel.channel_point }}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }}{{ channel.remote_balance|intcomma }}{{ channel.limbo_balance|intcomma }}{{ channel.local_commit_fee_sat }}{{ channel.closing_txid }}
+
+{% endif %} +{% if pending_force_closed %} +
+

Pending Force Close Channels

+ + + + + + + + + + + + + {% for channel in pending_force_closed %} + + + + {% with funding_txid=channel.channel_point|slice:":-2" %} + + {% endwith %} + + + + + + + + {% endfor %} +
Channel IDPeer AliasChannel PointCapacityLocal BalanceRemote BalanceBalance In LimboMaturityClosing TX
{{ channel.chan_id }}{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}{{ channel.channel_point }}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }}{{ channel.remote_balance|intcomma }}{{ channel.limbo_balance|intcomma }}{{ channel.maturity_datetime|naturaltime }}{{ channel.closing_txid }}
+
+{% endif %} +{% if waiting_for_close %} +
+

Channels Waiting To Close

+ + + + + + + + + + + + + {% for channel in waiting_for_close %} + + + + {% with funding_txid=channel.channel_point|slice:":-2" %} + + {% endwith %} + + + + + + + + {% endfor %} +
Channel IDPeer AliasChannel PointCapacityLocal BalanceRemote BalanceBalance In LimboLocal Commit FeeClosing TX
{{ channel.chan_id }}{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}{{ channel.channel_point }}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }}{{ channel.remote_balance|intcomma }}{{ channel.limbo_balance|intcomma }}{{ channel.local_commit_fee_sat }}{{ channel.closing_txid }}
+
+{% endif %} {% if closures %}

Closures

@@ -18,6 +117,7 @@ Opener Closer Resolutions + Costs {% for closure in closures %} @@ -32,6 +132,20 @@ {% if closure.open_initiator == 0 %}Unknown{% elif closure.open_initiator == 1 %}Local{% elif closure.open_initiator == 2 %}Remote{% elif closure.open_initiator == 3 %}Both{% else %}{{ closure.open_initiator }}{% endif %} {% 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 == '' %} + --- + {% elif closure.open_initiator == 2 and closure.close_type == 0 %} + --- + {% else %} +
+ {% csrf_token %} + + + +
+ {% endif %} + {% endfor %} @@ -42,4 +156,4 @@

No channel closures found!

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/failed_htlcs.html b/gui/templates/failed_htlcs.html index 4c11bea..8288ab3 100644 --- a/gui/templates/failed_htlcs.html +++ b/gui/templates/failed_htlcs.html @@ -21,8 +21,8 @@ {% for failed_htlc in failed_htlcs %} {{ failed_htlc.timestamp|naturaltime }} - {{ failed_htlc.chan_id_in }} - {{ failed_htlc.chan_id_out }} + {{ 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 %} {{ failed_htlc.amount|intcomma }} @@ -40,4 +40,4 @@

You dont have any failed HTLCs yet.

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/home.html b/gui/templates/home.html index 6381c21..0c0ebcd 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -4,30 +4,28 @@ {% load humanize %}

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

+

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

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

-

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

+

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

{% csrf_token %} - +
-

Completed Payments: {{ total_payments }} | Sats Sent: {{ total_sent|intcomma }} | Fees Paid: {{ fees_paid|intcomma }} [{{ payments_ppm|intcomma }}]

-

Paid Invoices: {{ total_invoices }} | Sats Received: {{ total_received|intcomma }}

-

Lifetime Routed: {{ total_forwards|intcomma }} | Value: {{ total_value_forwards|intcomma }} | Fees Earned: {{ earned|intcomma }} [{{ routed_ppm|intcomma }}] | Onchain Fees: {{ onchain_costs|intcomma }} | Percent Cost: {{ percent_cost }}%

-

7-Day Routed: {{ routed_7day|intcomma }} | Value: {{ routed_7day_amt|intcomma }} | Fees Earned: {{ earned_7day|intcomma }} [{{ 7day_routed_ppm|intcomma }}] | Onchain Fees: {{ onchain_costs_7day|intcomma }} | Offchain Fees: {{ total_7day_fees|intcomma }} [{{ 7day_payments_ppm|intcomma }}] | Percent Cost: {{ percent_cost_7day }}% | Profit/Outbound: {{ profit_per_outbound|intcomma }} [{{ profit_per_outbound_real|intcomma }}] | Outbound Utilization: {{ routed_7day_percent }}%

+

1-Day Routed: {{ routed_1day|intcomma }} | Value: {{ routed_1day_amt|intcomma }} | Fees Earned: {{ earned_1day|intcomma }} [{{ 1day_routed_ppm|intcomma }}] | Onchain Fees: {{ onchain_costs_1day|intcomma }} | Offchain Fees: {{ total_1day_fees|intcomma }} [{{ 1day_payments_ppm|intcomma }}] | Percent Cost: {{ percent_cost_1day }}% | Profit/Outbound: {{ profit_per_outbound_1d|intcomma }} [{{ profit_per_outbound_real_1d|intcomma }}] | Outbound Utilization: {{ routed_1day_percent }}%

+

7-Day Routed: {{ routed_7day|intcomma }} | Value: {{ routed_7day_amt|intcomma }} | Fees Earned: {{ earned_7day|intcomma }} [{{ 7day_routed_ppm|intcomma }}] | Onchain Fees: {{ onchain_costs_7day|intcomma }} | Offchain Fees: {{ total_7day_fees|intcomma }} [{{ 7day_payments_ppm|intcomma }}] | Percent Cost: {{ percent_cost_7day }}% | Profit/Outbound: {{ profit_per_outbound_7d|intcomma }} [{{ profit_per_outbound_real_7d|intcomma }}] | Outbound Utilization: {{ routed_7day_percent }}%

Total Inbound Liquidity: {{ sum_inbound|intcomma }} | Outbound Liquidity: {{ sum_outbound|intcomma }} | Liquidity Ratio: {{ liq_ratio }}%

Active Inbound Liquidity: {{ inbound|intcomma }} | Outbound Liquidity: {{ outbound|intcomma }} | Unsettled Liquidity: {{ unsettled|intcomma }}

Inactive Inbound Liquidity: {{ inactive_inbound|intcomma }} | Outbound Liquidity: {{ inactive_outbound|intcomma }} | Unsettled Liquidity: {{ inactive_unsettled|intcomma }}

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

-

Closures | New Peers | AR Actions | Fee Rates | Autopilot | Autofees | Channel Performance | Keysends | Rebalancing | Towers | Batching | Advanced Settings

+

P&L | Closures | New Peers | AR Actions | Fee Rates | Autopilot | Autofees | Channel Performance | Keysends | Rebalancing | Towers | Batching | Advanced Settings

{% if active_channels %}
@@ -44,10 +42,10 @@ Unsettled oRate oBase + o1D + i1D o7D i7D - oLife - iLife iRate iBase iTarget% @@ -64,10 +62,10 @@ {{ channel.unsettled_balance|intcomma }} ({{ channel.htlc_count }}) {{ channel.local_fee_rate|intcomma }} {{ channel.local_base_fee|intcomma }} + {{ channel.amt_routed_out_1day|intcomma }} M ({{ channel.routed_out_1day }}) + {{ channel.amt_routed_in_1day|intcomma }} M ({{ channel.routed_in_1day }}) {{ channel.amt_routed_out_7day|intcomma }} M ({{ channel.routed_out_7day }}) {{ channel.amt_routed_in_7day|intcomma }} M ({{ channel.routed_in_7day }}) - {{ channel.amt_routed_out|intcomma }} M ({{ channel.routed_out }}) - {{ channel.amt_routed_in|intcomma }} M ({{ channel.routed_in }}) {{ channel.remote_fee_rate|intcomma }} {{ channel.remote_base_fee|intcomma }} @@ -216,18 +214,106 @@ Capacity Local Balance Remote Balance - Commit Fee + AF + Fee Rate + Base Fee + CLTV + Amt + Max Cost% + oTarget% + iTarget% + AR {% for channel in pending_open %} {% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} - {% with funding_txid=channel.channel_point|slice:":-2" %} - {{ channel.channel_point }} - {% endwith %} - {{ channel.capacity|intcomma }} + {{ channel.channel_point }} + {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} {{ channel.remote_balance|intcomma }} - {{ channel.commit_fee }} + +
+ {% csrf_token %} + + + + + +
+ + +
+ {% csrf_token %} + + + + +
+ + +
+ {% csrf_token %} + + + + +
+ + +
+ {% csrf_token %} + + + + +
+ + +
+ {% csrf_token %} + + + + +
+ + +
+ {% csrf_token %} + + + + +
+ + +
+ {% csrf_token %} + + + + +
+ + +
+ {% csrf_token %} + + + + +
+ + +
+ {% csrf_token %} + + + + + +
+ {% endfor %} @@ -240,25 +326,23 @@ Channel ID Peer Alias - Channel Point Capacity Local Balance Remote Balance Balance In Limbo Local Commit Fee + Closing TX {% for channel in pending_closed %} - {{ channel.chan_id }} + {{ channel.chan_id }} {% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} - {% with funding_txid=channel.channel_point|slice:":-2" %} - {{ channel.channel_point }} - {% endwith %} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} {{ channel.remote_balance|intcomma }} {{ channel.limbo_balance|intcomma }} - {{ channel.commitments.local_commit_fee_sat }} + {{ channel.local_commit_fee_sat|intcomma }} + {{ channel.closing_txid }} {% endfor %} @@ -271,7 +355,6 @@ Channel ID Peer Alias - Channel Point Capacity Local Balance Remote Balance @@ -281,11 +364,8 @@ {% for channel in pending_force_closed %} - {{ channel.chan_id }} + {{ channel.chan_id }} {% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} - {% with funding_txid=channel.channel_point|slice:":-2" %} - {{ channel.channel_point }} - {% endwith %} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} {{ channel.remote_balance|intcomma }} @@ -304,25 +384,23 @@ Channel ID Peer Alias - Channel Point Capacity Local Balance Remote Balance Balance In Limbo Local Commit Fee + Closing TX {% for channel in waiting_for_close %} - {{ channel.chan_id }} + {{ channel.chan_id }} {% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} - {% with funding_txid=channel.channel_point|slice:":-2" %} - {{ channel.channel_point }} - {% endwith %} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} {{ channel.remote_balance|intcomma }} {{ channel.limbo_balance|intcomma }} - {{ channel.commitments.local_commit_fee_sat }} + {{ channel.local_commit_fee_sat|intcomma }} + {{ channel.closing_txid }} {% endfor %} @@ -352,7 +430,7 @@ {% if forward.chan_out_alias == '' %}---{% else %}{{ forward.chan_out_alias }}{% endif %} {{ forward.chan_id_in }} {{ forward.chan_id_out }} - {{ forward.fee }} + {{ forward.fee|intcomma }} {{ forward.ppm|intcomma }} {% endfor %} @@ -361,28 +439,32 @@ {% endif %} {% if rebalances %}
-

Last 10 Rebalance Requests (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing)

+

Last 10 Rebalance Requests (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing via {{ available_count }} outbound channels)

+ + {% for rebalance in rebalances %} - - + + + + @@ -414,9 +496,9 @@ - - - + + + {% endfor %} @@ -441,13 +523,13 @@ {% for invoice in invoices %} - + - + - - + + {% endfor %} @@ -517,6 +599,8 @@ + + @@ -527,6 +611,10 @@ + + + + diff --git a/gui/templates/income.html b/gui/templates/income.html new file mode 100644 index 0000000..109dcc9 --- /dev/null +++ b/gui/templates/income.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - P&L{% endblock %} +{% block content %} +{% load humanize %} +
+

P&L Statement For {% if node_info.alias != "" %}{{ node_info.alias }} ({{ node_info.identity_pubkey }}){% else %}{{ node_info.identity_pubkey }}{% endif %}

+
Requested Start Stop Scheduled DurationActual Duration Value Fee Limit Target PPMFees Paid Last Hop Alias Status
{{ rebalance.requested|naturaltime }}{% if rebalance.status == 0 %}N/A{% else %}{{ rebalance.start|naturaltime }}{% endif %}{% if rebalance.status > 1 %}{{ rebalance.stop|naturaltime }}{% else %}N/A{% endif %}{% if rebalance.status == 0 %}---{% else %}{{ rebalance.start|naturaltime }}{% endif %}{% if rebalance.status > 1 %}{{ 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 %}
{{ payment.fee|intcomma }} {{ payment.ppm|intcomma }} {% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %}{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %}{% if payment.status == 2 %}{% if payment.chan_out != 'MPP' %}{{ payment.chan_out }}{% else %}{{ payment.chan_out }}{% endif %}{% else %}N/A{% endif %}{% if payment.status == 2 %}Open{% else %}N/A{% endif %}{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}---{% endif %}{% if payment.status == 2 %}{% if payment.chan_out != 'MPP' %}{{ payment.chan_out }}{% else %}{{ payment.chan_out }}{% endif %}{% else %}---{% endif %}{% if payment.status == 2 %}Open{% else %}---{% endif %} {% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %}
{{ invoice.creation_date|naturaltime }}{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}N/A{% endif %}{% 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 %}N/A{% endif %}{% 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 %}N/A{% endif %}{% if invoice.state == 1 and invoice.chan_in != None %}{{ invoice.chan_in }}{% else %}N/A{% 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 %}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Line Item1 Day7 Day30 Day90 DayLifetime
Payments Routed{{ forward_count_1day|intcomma }}{{ forward_count_7day|intcomma }}{{ forward_count_30day|intcomma }}{{ forward_count_90day|intcomma }}{{ forward_count|intcomma }}
Value Routed{{ forward_amount_1day|intcomma }}{{ forward_amount_7day|intcomma }}{{ forward_amount_30day|intcomma }}{{ forward_amount_90day|intcomma }}{{ forward_amount|intcomma }}
Revenue Earned{{ total_revenue_1day|intcomma }} [{{ total_revenue_ppm_1day|intcomma }}]{{ total_revenue_7day|intcomma }} [{{ total_revenue_ppm_7day|intcomma }}]{{ total_revenue_30day|intcomma }} [{{ total_revenue_ppm_30day|intcomma }}]{{ total_revenue_90day|intcomma }} [{{ total_revenue_ppm_90day|intcomma }}]{{ total_revenue|intcomma }} [{{ total_revenue_ppm|intcomma }}]
Onchain Costs{{ onchain_costs_1day|intcomma }}{{ onchain_costs_7day|intcomma }}{{ onchain_costs_30day|intcomma }}{{ onchain_costs_90day|intcomma }}{{ onchain_costs|intcomma }}
Offchain Costs{{ total_fees_1day|intcomma }} [{{ total_fees_ppm_1day }}]{{ total_fees_7day|intcomma }} [{{ total_fees_ppm_7day }}]{{ total_fees_30day|intcomma }} [{{ total_fees_ppm_30day }}]{{ total_fees_90day|intcomma }} [{{ total_fees_ppm_90day }}]{{ total_fees|intcomma }} [{{ total_fees_ppm }}]
Percent Cost{{ percent_cost_1day|intcomma }}%{{ percent_cost_7day|intcomma }}%{{ percent_cost_30day|intcomma }}%{{ percent_cost_90day|intcomma }}%{{ percent_cost|intcomma }}%
Profits{{ profits_1day|intcomma }} [{{ profits_ppm_1day }}]{{ profits_7day|intcomma }} [{{ profits_ppm_7day }}]{{ profits_30day|intcomma }} [{{ profits_ppm_30day }}]{{ profits_90day|intcomma }} [{{ profits_ppm_90day }}]{{ profits|intcomma }} [{{ profits_ppm }}]
+
+{% endblock %} \ No newline at end of file diff --git a/gui/templates/invoices.html b/gui/templates/invoices.html index 0a306ef..fa799aa 100644 --- a/gui/templates/invoices.html +++ b/gui/templates/invoices.html @@ -20,13 +20,13 @@ {% for invoice in invoices %} {{ invoice.creation_date|naturaltime }} - {% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}N/A{% endif %} + {% 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 %}N/A{% endif %} + {% 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 %}N/A{% endif %} - {% if invoice.state == 1 and invoice.chan_in != None %}{{ invoice.chan_in }}{% else %}N/A{% 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 %} {% endfor %} @@ -38,4 +38,4 @@

You dont have any invoices yet.

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/open_list.html b/gui/templates/open_list.html index 0f24d3e..e023d32 100644 --- a/gui/templates/open_list.html +++ b/gui/templates/open_list.html @@ -21,7 +21,7 @@ {{ node.node_pubkey }} {% if node.alias == '' %}---{% else %}{{ node.alias }}{% endif %} - {{ node.count }} + {{ node.count }} {{ node.amount|add:"0"|intcomma }} {{ node.fees|add:"0"|intcomma }} {{ node.ppm|add:"0"|intcomma }} @@ -38,4 +38,4 @@

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

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/payments.html b/gui/templates/payments.html index ed6d4bc..bf7ad3d 100644 --- a/gui/templates/payments.html +++ b/gui/templates/payments.html @@ -26,9 +26,9 @@ {{ payment.fee|intcomma }} {{ payment.ppm|intcomma }} {% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %} - {% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %} - {% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %} - {% if payment.status == 2 %}Open{% else %}N/A{% endif %} + {% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}---{% endif %} + {% if payment.status == 2 %}{{ payment.chan_out }}{% else %}---{% endif %} + {% if payment.status == 2 %}Open{% else %}---{% endif %} {% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %} {% endfor %} @@ -40,4 +40,4 @@

You dont have any payments yet.

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/pending_htlcs.html b/gui/templates/pending_htlcs.html index 85fe26c..babce0e 100644 --- a/gui/templates/pending_htlcs.html +++ b/gui/templates/pending_htlcs.html @@ -17,9 +17,9 @@ {% for htlc in outgoing_htlcs %} - {{ htlc.chan_id }} + {{ htlc.chan_id }} {% if htlc.alias == '' %}---{% else %}{{ htlc.alias }}{% endif %} - {{ htlc.forwarding_channel }} + {% 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 @@ -44,9 +44,9 @@ {% for htlc in incoming_htlcs %} - {{ htlc.chan_id }} + {{ htlc.chan_id }} {% if htlc.alias == '' %}---{% else %}{{ htlc.alias }}{% endif %} - {{ htlc.forwarding_channel }} + {% 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 @@ -61,4 +61,4 @@

No pending HTLCs were found!

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/rebalances.html b/gui/templates/rebalances.html new file mode 100644 index 0000000..2e23f7e --- /dev/null +++ b/gui/templates/rebalances.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Rebalances{% endblock %} +{% block content %} +{% load humanize %} +{% if rebalances %} +
+

Last 150 Rebalances

+ + + + + + + + + + + + + + + {% 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 not rebalances %} +
+

You dont have any rebalances yet.

+
+{% endif %} +{% endblock %} diff --git a/gui/templates/rebalancing.html b/gui/templates/rebalancing.html index 63927b6..284d605 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)

+

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

@@ -21,7 +21,7 @@ - + @@ -94,28 +94,32 @@ {% endif %} {% if rebalancer %}
-

Last 20 Rebalance Requests

+

Last 20 Rebalance Requests

Target Amt Max Cost % oTarget%iTarget%iTarget% AR 7-Day Rate Active
+ + {% for rebalance in rebalancer %} - - + + + + @@ -158,6 +162,8 @@ + + @@ -168,6 +174,10 @@ + + + + @@ -197,4 +207,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/templates/resolutions.html b/gui/templates/resolutions.html index e9805d8..123e2a4 100644 --- a/gui/templates/resolutions.html +++ b/gui/templates/resolutions.html @@ -19,7 +19,7 @@ - + {% endfor %}
Requested Start Stop Scheduled DurationActual Duration Value Fee Limit Target PPMFees Paid Last Hop Alias Status
{{ rebalance.requested|naturaltime }}N/A{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %} 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>N/A{% 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 == 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 resolution.outcome == 0 %}Unknown{% elif resolution.outcome == 1 %}Claimed{% elif resolution.outcome == 2 %}Unclaimed{% elif resolution.outcome == 3 %}Abandoned{% elif resolution.outcome == 4 %}First Stage{% elif resolution.outcome == 5 %}Timeout{% else %}{{ resolution.outcome }}{% endif %} {{ resolution.outpoint_tx }}:{{ resolution.outpoint_index }} {{ resolution.amount_sat|intcomma }}{{ resolution.sweep_txid }}{% if resolution.resolution_type != 2 %}{{ resolution.sweep_txid }}{% else %}---{% endif %}
diff --git a/gui/templates/route.html b/gui/templates/route.html index e584834..ddefd13 100644 --- a/gui/templates/route.html +++ b/gui/templates/route.html @@ -4,7 +4,7 @@ {% load humanize %} {% if route %}
-

Route For Payment: {{ payment_hash }}

+

Route For : {{ payment_hash }}

@@ -12,6 +12,7 @@ + @@ -23,8 +24,9 @@ + - + {% endfor %} @@ -36,4 +38,4 @@

A route was not found for this payment hash!

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/gui/urls.py b/gui/urls.py index 878228a..8666d5d 100644 --- a/gui/urls.py +++ b/gui/urls.py @@ -21,6 +21,7 @@ router.register(r'failedhtlcs', views.FailedHTLCViewSet) urlpatterns = [ path('', views.home, name='home'), path('route', views.route, name='route'), + path('routes', views.routes, name='routes'), path('peers', views.peers, name='peers'), path('balances', views.balances, name='balances'), path('closures', views.closures, name='closures'), @@ -34,6 +35,8 @@ urlpatterns = [ path('payments', views.payments, name='payments'), path('invoices', views.invoices, name='invoices'), path('forwards', views.forwards, name='forwards'), + path('income', views.income, name='income'), + path('rebalances', views.rebalances, name='rebalances'), path('rebalancing', views.rebalancing, name='rebalancing'), path('openchannel/', views.open_channel_form, name='open-channel-form'), path('closechannel/', views.close_channel_form, name='close-channel-form'), @@ -47,6 +50,7 @@ urlpatterns = [ path('updatechanpolicy/', views.update_chan_policy, name='updatechanpolicy'), path('autorebalance/', views.auto_rebalance, name='auto-rebalance'), 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('opens/', views.opens, name='opens'), path('actions/', views.actions, name='actions'), @@ -68,4 +72,4 @@ urlpatterns = [ path('api/balances/', views.api_balances, name='api-balances'), path('api/pendingchannels/', views.pending_channels, name='pending-channels'), path('lndg-admin/', admin.site.urls), -] \ No newline at end of file +] diff --git a/gui/views.py b/gui/views.py index e80d1a8..559c504 100644 --- a/gui/views.py +++ b/gui/views.py @@ -1,6 +1,6 @@ from django.contrib import messages from django.shortcuts import get_object_or_404, render, redirect -from django.db.models import Sum, IntegerField, FloatField, Count, F, Q +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 @@ -8,8 +8,8 @@ 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 -from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, Closures, Resolutions, PendingHTLCs, FailedHTLCs, Autopilot, Autofees +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 .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 @@ -53,38 +53,75 @@ def home(request): pending_closed = None pending_force_closed = None waiting_for_close = None + pending_open_balance = 0 if pending_channels.pending_open_channels: target_resp = pending_channels.pending_open_channels peers = Peers.objects.all() - pending_open = [{'alias':peers.filter(pubkey=target_resp[i].channel.remote_node_pub)[0].alias if peers.filter(pubkey=target_resp[i].channel.remote_node_pub).exists() else None,'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type,'commit_fee':target_resp[i].commit_fee,'commit_weight':target_resp[i].commit_weight,'fee_per_kw':target_resp[i].fee_per_kw} for i in range(0,len(target_resp))] + pending_changes = PendingChannels.objects.all() + pending_open = [] + inbound_setting = int(LocalSettings.objects.filter(key='AR-Inbound%')[0].value) if LocalSettings.objects.filter(key='AR-Inbound%').exists() else 100 + outbound_setting = int(LocalSettings.objects.filter(key='AR-Outbound%')[0].value) if LocalSettings.objects.filter(key='AR-Outbound%').exists() else 75 + amt_setting = float(LocalSettings.objects.filter(key='AR-Target%')[0].value) if LocalSettings.objects.filter(key='AR-Target%').exists() else 5 + cost_setting = int(LocalSettings.objects.filter(key='AR-MaxCost%')[0].value) if LocalSettings.objects.filter(key='AR-MaxCost%').exists() else 65 + auto_fees = int(LocalSettings.objects.filter(key='AF-Enabled')[0].value) if LocalSettings.objects.filter(key='AF-Enabled').exists() else 0 + for i in range(0,len(target_resp)): + item = {} + pending_open_balance += target_resp[i].channel.local_balance + funding_txid = target_resp[i].channel.channel_point.split(':')[0] + output_index = target_resp[i].channel.channel_point.split(':')[1] + updated = pending_changes.filter(funding_txid=funding_txid,output_index=output_index).exists() + item['alias'] = peers.filter(pubkey=target_resp[i].channel.remote_node_pub)[0].alias if peers.filter(pubkey=target_resp[i].channel.remote_node_pub).exists() else None + item['remote_node_pub'] = target_resp[i].channel.remote_node_pub + item['channel_point'] = target_resp[i].channel.channel_point + item['funding_txid'] = funding_txid + item['output_index'] = output_index + item['capacity'] = target_resp[i].channel.capacity + item['local_balance'] = target_resp[i].channel.local_balance + item['remote_balance'] = target_resp[i].channel.remote_balance + item['local_chan_reserve_sat'] = target_resp[i].channel.local_chan_reserve_sat + item['remote_chan_reserve_sat'] = target_resp[i].channel.remote_chan_reserve_sat + item['initiator'] = target_resp[i].channel.initiator + item['commitment_type'] = target_resp[i].channel.commitment_type + item['commit_fee'] = target_resp[i].commit_fee + item['commit_weight'] = target_resp[i].commit_weight + item['fee_per_kw'] = target_resp[i].fee_per_kw + item['local_base_fee'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].local_base_fee if updated else '' + item['local_fee_rate'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].local_fee_rate if updated else '' + item['local_cltv'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].local_cltv if updated else '' + item['auto_rebalance'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].auto_rebalance if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].auto_rebalance != None else False + item['ar_amt_target'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_amt_target if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_amt_target != None else int((amt_setting/100) * target_resp[i].channel.capacity) + item['ar_in_target'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_in_target if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_in_target != None else inbound_setting + item['ar_out_target'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_out_target if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_out_target != None else outbound_setting + item['ar_max_cost'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_max_cost if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].ar_max_cost != None else cost_setting + item['auto_fees'] = pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].auto_fees if updated and pending_changes.filter(funding_txid=funding_txid,output_index=output_index)[0].auto_fees != None else (False if auto_fees == 0 else True) + pending_open.append(item) if pending_channels.pending_closing_channels: target_resp = pending_channels.pending_closing_channels - pending_closed = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type,'limbo_balance':target_resp[i].limbo_balance} for i in range(0,len(target_resp))] + pending_closed = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0],output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None, + 'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0],output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None, + 'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat, + 'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type, 'local_commit_fee_sat': target_resp[i].commitments.local_commit_fee_sat,'limbo_balance':target_resp[i].limbo_balance, + 'closing_txid':target_resp[i].closing_txid} for i in range(0,len(target_resp))] if pending_channels.pending_force_closing_channels: target_resp = pending_channels.pending_force_closing_channels - pending_force_closed = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type,'closing_txid':target_resp[i].closing_txid,'limbo_balance':target_resp[i].limbo_balance,'maturity_height':target_resp[i].maturity_height,'blocks_til_maturity':target_resp[i].blocks_til_maturity,'maturity_datetime':(datetime.now()+timedelta(minutes=(10*target_resp[i].blocks_til_maturity)))} for i in range(0,len(target_resp))] + pending_force_closed = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0],output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None, + 'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0],output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None, + 'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'initiator':target_resp[i].channel.initiator, + 'commitment_type':target_resp[i].channel.commitment_type,'closing_txid':target_resp[i].closing_txid,'limbo_balance':target_resp[i].limbo_balance,'maturity_height':target_resp[i].maturity_height,'blocks_til_maturity':target_resp[i].blocks_til_maturity if target_resp[i].blocks_til_maturity > 0 else find_next_block_maturity(target_resp[i]), + 'maturity_datetime':(datetime.now()+timedelta(minutes=(10*target_resp[i].blocks_til_maturity if target_resp[i].blocks_til_maturity > 0 else 10*find_next_block_maturity(target_resp[i]) )))} for i in range(0,len(target_resp))] if pending_channels.waiting_close_channels: target_resp = pending_channels.waiting_close_channels - waiting_for_close = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type,'limbo_balance':target_resp[i].limbo_balance} for i in range(0,len(target_resp))] + waiting_for_close = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None, + 'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None, + 'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat, + 'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type, 'local_commit_fee_sat': target_resp[i].commitments.local_commit_fee_sat, 'limbo_balance':target_resp[i].limbo_balance, + 'closing_txid':target_resp[i].closing_txid} for i in range(0,len(target_resp))] #Get recorded payment events payments = Payments.objects.exclude(status=3) - total_payments = payments.filter(status=2).count() - total_sent = 0 if total_payments == 0 else payments.filter(status=2).aggregate(Sum('value'))['value__sum'] - total_fees = 0 if total_payments == 0 else payments.aggregate(Sum('fee'))['fee__sum'] #Get recorded invoice details invoices = Invoices.objects.exclude(state=2) - total_invoices = invoices.filter(state=1).count() - total_received = 0 if total_invoices == 0 else invoices.aggregate(Sum('amt_paid'))['amt_paid__sum'] #Get recorded forwarding events forwards = Forwards.objects.all().annotate(amt_in=Sum('amt_in_msat')/1000).annotate(amt_out=Sum('amt_out_msat')/1000).annotate(ppm=Round((Sum('fee')*1000000000)/Sum('amt_out_msat'), output_field=IntegerField())).order_by('-id') - forwards_df = DataFrame.from_records(forwards.values()) - total_forwards = forwards_df.shape[0] - total_value_forwards = 0 if total_forwards == 0 else int(forwards_df['amt_out_msat'].sum()/1000) - total_earned = 0 if total_forwards == 0 else forwards_df['fee'].sum() - forwards_df_in_sum = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_in', as_index=True).sum() - forwards_df_out_sum = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_out', as_index=True).sum() - forwards_df_in_count = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_in', as_index=True).count() - forwards_df_out_count = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_out', as_index=True).count() #Get current active channels active_channels = channels.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')).order_by('outbound_percent') active_capacity = 0 if active_channels.count() == 0 else active_channels.aggregate(Sum('capacity'))['capacity__sum'] @@ -92,20 +129,33 @@ def home(request): active_outbound = 0 if active_capacity == 0 else active_channels.aggregate(Sum('local_balance'))['local_balance__sum'] active_unsettled = 0 if active_capacity == 0 else active_channels.aggregate(Sum('unsettled_balance'))['unsettled_balance__sum'] filter_7day = datetime.now() - timedelta(days=7) + filter_1day = datetime.now() - timedelta(days=1) forwards_df_7d = DataFrame.from_records(forwards.filter(forward_date__gte=filter_7day).values()) + forwards_df_1d = DataFrame() if forwards_df_7d.empty else forwards_df_7d[forwards_df_7d['forward_date']>=filter_1day] forwards_df_in_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_in', as_index=True).sum() forwards_df_out_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_out', as_index=True).sum() forwards_df_in_7d_count = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_in', as_index=True).count() forwards_df_out_7d_count = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_out', as_index=True).count() + forwards_df_in_1d_sum = DataFrame() if forwards_df_1d.empty else forwards_df_1d.groupby('chan_id_in', as_index=True).sum() + forwards_df_out_1d_sum = DataFrame() if forwards_df_1d.empty else forwards_df_1d.groupby('chan_id_out', as_index=True).sum() + forwards_df_in_1d_count = DataFrame() if forwards_df_1d.empty else forwards_df_1d.groupby('chan_id_in', as_index=True).count() + forwards_df_out_1d_count = DataFrame() if forwards_df_1d.empty else forwards_df_1d.groupby('chan_id_out', as_index=True).count() + routed_1day = forwards_df_1d.shape[0] routed_7day = forwards_df_7d.shape[0] routed_7day_amt = 0 if routed_7day == 0 else int(forwards_df_7d['amt_out_msat'].sum()/1000) + routed_1day_amt = 0 if routed_1day == 0 else int(forwards_df_1d['amt_out_msat'].sum()/1000) total_earned_7day = 0 if routed_7day == 0 else forwards_df_7d['fee'].sum() + total_earned_1day = 0 if routed_1day == 0 else forwards_df_1d['fee'].sum() payments_7day = payments.filter(status=2).filter(creation_date__gte=filter_7day) payments_7day_amt = 0 if payments_7day.count() == 0 else payments_7day.aggregate(Sum('value'))['value__sum'] + payments_1day = payments.filter(status=2).filter(creation_date__gte=filter_1day) + payments_1day_amt = 0 if payments_1day.count() == 0 else payments_1day.aggregate(Sum('value'))['value__sum'] total_7day_fees = 0 if payments_7day.count() == 0 else payments_7day.aggregate(Sum('fee'))['fee__sum'] + total_1day_fees = 0 if payments_1day.count() == 0 else payments_1day.aggregate(Sum('fee'))['fee__sum'] pending_htlc_count = channels.filter(is_open=True).aggregate(Sum('htlc_count'))['htlc_count__sum'] if channels.filter(is_open=True).exists() else 0 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 detailed_active_channels = [] for channel in active_channels: detailed_channel = {} @@ -128,14 +178,14 @@ def home(request): detailed_channel['output_index'] = channel.output_index detailed_channel['outbound_percent'] = int(round(channel.outbound_percent/10, 0)) detailed_channel['inbound_percent'] = int(round(channel.inbound_percent/10, 0)) - detailed_channel['routed_in'] = forwards_df_in_count.loc[channel.chan_id].amt_out_msat if (forwards_df_in_count.index == channel.chan_id).any() else 0 - detailed_channel['routed_out'] = forwards_df_out_count.loc[channel.chan_id].amt_out_msat if (forwards_df_out_count.index == channel.chan_id).any() else 0 - detailed_channel['amt_routed_in'] = int(forwards_df_in_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_in_sum.index == channel.chan_id).any() else 0 - detailed_channel['amt_routed_out'] = int(forwards_df_out_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_out_sum.index == channel.chan_id).any() else 0 detailed_channel['routed_in_7day'] = forwards_df_in_7d_count.loc[channel.chan_id].amt_out_msat if (forwards_df_in_7d_count.index == channel.chan_id).any() else 0 detailed_channel['routed_out_7day'] = forwards_df_out_7d_count.loc[channel.chan_id].amt_out_msat if (forwards_df_out_7d_count.index == channel.chan_id).any() else 0 detailed_channel['amt_routed_in_7day'] = int(forwards_df_in_7d_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_in_7d_sum.index == channel.chan_id).any() else 0 detailed_channel['amt_routed_out_7day'] = int(forwards_df_out_7d_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_out_7d_sum.index == channel.chan_id).any() else 0 + detailed_channel['routed_in_1day'] = forwards_df_in_1d_count.loc[channel.chan_id].amt_out_msat if (forwards_df_in_1d_count.index == channel.chan_id).any() else 0 + detailed_channel['routed_out_1day'] = forwards_df_out_1d_count.loc[channel.chan_id].amt_out_msat if (forwards_df_out_1d_count.index == channel.chan_id).any() else 0 + detailed_channel['amt_routed_in_1day'] = int(forwards_df_in_1d_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_in_1d_sum.index == channel.chan_id).any() else 0 + detailed_channel['amt_routed_out_1day'] = int(forwards_df_out_1d_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_out_1d_sum.index == channel.chan_id).any() else 0 detailed_channel['htlc_count'] = channel.htlc_count detailed_channel['auto_rebalance'] = channel.auto_rebalance detailed_channel['ar_in_target'] = channel.ar_in_target @@ -153,10 +203,16 @@ def home(request): sum_outbound = active_outbound + pending_outbound + inactive_outbound sum_inbound = active_inbound + pending_inbound + inactive_inbound onchain_txs = Onchain.objects.all() - onchain_costs = 0 if onchain_txs.count() == 0 else onchain_txs.aggregate(Sum('fee'))['fee__sum'] 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'] - total_costs = total_fees + onchain_costs + 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 = 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 = channels.filter(chan_id__in=closures_7day.values('chan_id')).aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_7day.exists() else 0 + close_fees_1day = channels.filter(chan_id__in=closures_1day.values('chan_id')).aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_1day.exists() else 0 + onchain_costs_7day += close_fees_7day + onchain_costs_1day += close_fees_1day total_costs_7day = total_7day_fees + onchain_costs_7day + 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') total_channels = node_info.num_active_channels + node_info.num_inactive_channels - private_count @@ -170,27 +226,27 @@ def home(request): 'node_info': node_info, 'total_channels': total_channels, 'balances': balances, + 'total_balance': balances.total_balance + sum_outbound + pending_open_balance + limbo_balance + private_outbound, 'payments': payments.annotate(ppm=Round((Sum('fee')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-creation_date')[:6], - 'total_sent': int(total_sent), - 'fees_paid': int(total_fees), - 'total_payments': total_payments, 'invoices': invoices.order_by('-creation_date')[:6], - 'total_received': total_received, - 'total_invoices': total_invoices, - 'forwards': forwards_df.head(15).to_dict(orient='records'), - 'earned': int(total_earned), - 'total_forwards': total_forwards, - 'total_value_forwards': total_value_forwards, + 'forwards': forwards[:15], + 'routed_1day': routed_1day, 'routed_7day': routed_7day, + 'routed_1day_amt': routed_1day_amt, 'routed_7day_amt': routed_7day_amt, + 'earned_1day': int(total_earned_1day), 'earned_7day': int(total_earned_7day), + 'routed_1day_percent': 0 if sum_outbound == 0 else int((routed_1day_amt/sum_outbound)*100), 'routed_7day_percent': 0 if sum_outbound == 0 else int((routed_7day_amt/sum_outbound)*100), - 'profit_per_outbound': 0 if sum_outbound == 0 else int((total_earned_7day - total_7day_fees)/(sum_outbound/1000000)), - 'profit_per_outbound_real': 0 if sum_outbound == 0 else int((total_earned_7day - total_costs_7day)/(sum_outbound/1000000)), - 'percent_cost': 0 if total_earned == 0 else int((total_costs/total_earned)*100), + 'profit_per_outbound_1d': 0 if sum_outbound == 0 else int((total_earned_1day - total_1day_fees)/(sum_outbound/1000000)), + 'profit_per_outbound_real_1d': 0 if sum_outbound == 0 else int((total_earned_1day - total_costs_1day)/(sum_outbound/1000000)), + 'profit_per_outbound_7d': 0 if sum_outbound == 0 else int((total_earned_7day - total_7day_fees)/(sum_outbound/1000000)), + 'profit_per_outbound_real_7d': 0 if sum_outbound == 0 else int((total_earned_7day - total_costs_7day)/(sum_outbound/1000000)), + 'percent_cost_1day': 0 if total_earned_1day == 0 else int((total_costs_1day/total_earned_1day)*100), 'percent_cost_7day': 0 if total_earned_7day == 0 else int((total_costs_7day/total_earned_7day)*100), - 'onchain_costs': onchain_costs, + 'onchain_costs_1day': onchain_costs_1day, 'onchain_costs_7day': onchain_costs_7day, + 'total_1day_fees': int(total_1day_fees), 'total_7day_fees': int(total_7day_fees), 'active_channels': detailed_active_channels, 'total_capacity': active_capacity + inactive_capacity, @@ -219,17 +275,19 @@ def home(request): 'local_settings': local_settings, 'pending_htlc_count': pending_htlc_count, 'failed_htlcs': FailedHTLCs.objects.all().order_by('-id')[:10], - 'payments_ppm': 0 if total_sent == 0 else int((total_fees/total_sent)*1000000), - 'routed_ppm': 0 if total_value_forwards == 0 else int((total_earned/total_value_forwards)*1000000), + '1day_routed_ppm': 0 if routed_1day_amt == 0 else int((total_earned_1day/routed_1day_amt)*1000000), '7day_routed_ppm': 0 if routed_7day_amt == 0 else int((total_earned_7day/routed_7day_amt)*1000000), + '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(), '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 '', 'graph_links': graph_links(), 'network_links': network_links(), - 'db_size': db_size + 'db_size': db_size, + 'num_updates': num_updates } return render(request, 'home.html', context) except Exception as e: @@ -325,7 +383,7 @@ def channels(request): apy_7day = 0 apy_30day = 0 context = { - 'channels': channels_df.to_dict(orient='records'), + '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 '', @@ -463,6 +521,22 @@ def route(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') +def routes(request): + if request.method == 'GET': + try: + pubkey = request.GET.urlencode()[1:] + context = { + 'payment_hash': pubkey, + 'route': PaymentHops.objects.filter(payment_hash__in=PaymentHops.objects.filter(node_pubkey=pubkey).order_by('-id').values_list('payment_hash')[:69]).annotate(ppm=Round((Sum('fee')/Sum('amt'))*1000000, output_field=IntegerField())) + } + return render(request, 'route.html', context) + except Exception as e: + error = str(e) + return render(request, 'error.html', {'error': error}) + else: + return redirect('home') + @login_required(login_url='/lndg-admin/login/?next=/') def peers(request): if request.method == 'GET': @@ -494,27 +568,63 @@ def balances(request): @login_required(login_url='/lndg-admin/login/?next=/') def closures(request): if request.method == 'GET': - closures_df = DataFrame.from_records(Closures.objects.all().values()) - if closures_df.empty: - merged = DataFrame() - else: - channels_df = DataFrame.from_records(Channels.objects.all().values('chan_id', 'alias')) - if channels_df.empty: - merged = closures_df - merged['alias'] = '' + try: + stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + pending_channels = stub.PendingChannels(ln.PendingChannelsRequest()) + channels = Channels.objects.all() + pending_closed = None + pending_force_closed = None + waiting_for_close = None + if pending_channels.pending_closing_channels: + target_resp = pending_channels.pending_closing_channels + pending_closed = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type, 'local_commit_fee_sat': target_resp[i].commitments.local_commit_fee_sat, 'limbo_balance':target_resp[i].limbo_balance, 'closing_txid':target_resp[i].closing_txid} for i in range(0,len(target_resp))] + if pending_channels.pending_force_closing_channels: + target_resp = pending_channels.pending_force_closing_channels + pending_force_closed = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type,'closing_txid':target_resp[i].closing_txid,'limbo_balance':target_resp[i].limbo_balance,'maturity_height':target_resp[i].maturity_height,'blocks_til_maturity':target_resp[i].blocks_til_maturity if target_resp[i].blocks_til_maturity > 0 else find_next_block_maturity(target_resp[i]),'maturity_datetime':(datetime.now()+timedelta(minutes=(10*target_resp[i].blocks_til_maturity if target_resp[i].blocks_til_maturity > 0 else 10*find_next_block_maturity(target_resp[i]) )))} for i in range(0,len(target_resp))] + if pending_channels.waiting_close_channels: + target_resp = pending_channels.waiting_close_channels + waiting_for_close = [{'chan_id':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].chan_id if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'alias':channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1])[0].alias if channels.filter(funding_txid=target_resp[i].channel.channel_point.split(':')[0], output_index=target_resp[i].channel.channel_point.split(':')[1]).exists() else None,'remote_node_pub':target_resp[i].channel.remote_node_pub,'channel_point':target_resp[i].channel.channel_point,'capacity':target_resp[i].channel.capacity,'local_balance':target_resp[i].channel.local_balance,'remote_balance':target_resp[i].channel.remote_balance,'local_chan_reserve_sat':target_resp[i].channel.local_chan_reserve_sat,'remote_chan_reserve_sat':target_resp[i].channel.remote_chan_reserve_sat,'initiator':target_resp[i].channel.initiator,'commitment_type':target_resp[i].channel.commitment_type, 'local_commit_fee_sat': target_resp[i].commitments.local_commit_fee_sat, 'limbo_balance':target_resp[i].limbo_balance, 'closing_txid':target_resp[i].closing_txid} for i in range(0,len(target_resp))] + closures_df = DataFrame.from_records(Closures.objects.all().values()) + if closures_df.empty: + merged = DataFrame() else: - merged = merge(closures_df, channels_df, on='chan_id', how='left') - merged['alias'] = merged['alias'].fillna('') - context = { - '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_links': network_links(), - 'graph_links': graph_links() - } - return render(request, 'closures.html', context) + channels_df = DataFrame.from_records(Channels.objects.all().values('chan_id', 'alias', 'closing_costs')) + 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_links': network_links(), + 'graph_links': graph_links() + } + return render(request, 'closures.html', context) + except Exception as e: + try: + error = str(e.code()) + except: + error = str(e) + return render(request, 'error.html', {'error': error}) else: return redirect('home') +def find_next_block_maturity(force_closing_channel): + #print (f"{datetime.now().strftime('%c')} : {force_closing_channel=}") + if force_closing_channel.blocks_til_maturity > 0: + return force_closing_channel.blocks_til_maturity + for pending_htlc in force_closing_channel.pending_htlcs: + if pending_htlc.blocks_til_maturity > 0: + #print (f"{datetime.now().strftime('%c')} : {pending_htlc=}") + return pending_htlc.blocks_til_maturity + return -1 + @login_required(login_url='/lndg-admin/login/?next=/') def towers(request): if request.method == 'GET': @@ -626,6 +736,150 @@ def resolutions(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') +def income(request): + if request.method == 'GET': + stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + 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() + payments = Payments.objects.filter(status=2) + payments_90day = payments.filter(creation_date__gte=filter_90day) + payments_30day = payments.filter(creation_date__gte=filter_30day) + payments_7day = payments.filter(creation_date__gte=filter_7day) + payments_1day = payments.filter(creation_date__gte=filter_1day) + onchain_txs = Onchain.objects.all() + onchain_txs_90day = onchain_txs.filter(time_stamp__gte=filter_90day) + onchain_txs_30day = onchain_txs.filter(time_stamp__gte=filter_30day) + onchain_txs_7day = onchain_txs.filter(time_stamp__gte=filter_7day) + onchain_txs_1day = onchain_txs.filter(time_stamp__gte=filter_1day) + closures = Closures.objects.all() + closures_90day = closures.filter(close_height__gte=(node_info.block_height - 12960)) + closures_30day = closures.filter(close_height__gte=(node_info.block_height - 4320)) + closures_7day = closures.filter(close_height__gte=(node_info.block_height - 1008)) + closures_1day = closures.filter(close_height__gte=(node_info.block_height - 144)) + forwards = Forwards.objects.all() + forwards_90day = forwards.filter(forward_date__gte=filter_90day) + forwards_30day = forwards.filter(forward_date__gte=filter_30day) + forwards_7day = forwards.filter(forward_date__gte=filter_7day) + forwards_1day = forwards.filter(forward_date__gte=filter_1day) + forward_count = forwards.count() + forward_count_90day = forwards_90day.count() + forward_count_30day = forwards_30day.count() + forward_count_7day = forwards_7day.count() + forward_count_1day = forwards_1day.count() + forward_amount = 0 if forward_count == 0 else int(forwards.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000) + forward_amount_90day = 0 if forward_count_90day == 0 else int(forwards_90day.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000) + forward_amount_30day = 0 if forward_count_30day == 0 else int(forwards_30day.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000) + forward_amount_7day = 0 if forward_count_7day == 0 else int(forwards_7day.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000) + forward_amount_1day = 0 if forward_count_1day == 0 else int(forwards_1day.aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/1000) + total_revenue = 0 if forward_count == 0 else int(forwards.aggregate(Sum('fee'))['fee__sum']) + total_revenue_90day = 0 if forward_count_90day == 0 else int(forwards_90day.aggregate(Sum('fee'))['fee__sum']) + total_revenue_30day = 0 if forward_count_30day == 0 else int(forwards_30day.aggregate(Sum('fee'))['fee__sum']) + total_revenue_7day = 0 if forward_count_7day == 0 else int(forwards_7day.aggregate(Sum('fee'))['fee__sum']) + total_revenue_1day = 0 if forward_count_1day == 0 else int(forwards_1day.aggregate(Sum('fee'))['fee__sum']) + total_revenue_ppm = 0 if forward_amount == 0 else int(total_revenue/(forward_amount/1000000)) + total_revenue_ppm_90day = 0 if forward_amount_90day == 0 else int(total_revenue_90day/(forward_amount_90day/1000000)) + total_revenue_ppm_30day = 0 if forward_amount_30day == 0 else int(total_revenue_30day/(forward_amount_30day/1000000)) + total_revenue_ppm_7day = 0 if forward_amount_7day == 0 else int(total_revenue_7day/(forward_amount_7day/1000000)) + total_revenue_ppm_1day = 0 if forward_amount_1day == 0 else int(total_revenue_1day/(forward_amount_1day/1000000)) + total_sent = 0 if payments.count() == 0 else int(payments.aggregate(Sum('value'))['value__sum']) + total_sent_90day = 0 if payments_90day.count() == 0 else int(payments_90day.aggregate(Sum('value'))['value__sum']) + total_sent_30day = 0 if payments_30day.count() == 0 else int(payments_30day.aggregate(Sum('value'))['value__sum']) + total_sent_7day = 0 if payments_7day.count() == 0 else int(payments_7day.aggregate(Sum('value'))['value__sum']) + total_sent_1day = 0 if payments_1day.count() == 0 else int(payments_1day.aggregate(Sum('value'))['value__sum']) + total_fees = 0 if payments.count() == 0 else int(payments.aggregate(Sum('fee'))['fee__sum']) + total_fees_90day = 0 if payments_90day.count() == 0 else int(payments_90day.aggregate(Sum('fee'))['fee__sum']) + total_fees_30day = 0 if payments_30day.count() == 0 else int(payments_30day.aggregate(Sum('fee'))['fee__sum']) + total_fees_7day = 0 if payments_7day.count() == 0 else int(payments_7day.aggregate(Sum('fee'))['fee__sum']) + total_fees_1day = 0 if payments_1day.count() == 0 else int(payments_1day.aggregate(Sum('fee'))['fee__sum']) + total_fees_ppm = 0 if total_sent == 0 else int(total_fees/(total_sent/1000000)) + total_fees_ppm_90day = 0 if total_sent_90day == 0 else int(total_fees_90day/(total_sent_90day/1000000)) + total_fees_ppm_30day = 0 if total_sent_30day == 0 else int(total_fees_30day/(total_sent_30day/1000000)) + total_fees_ppm_7day = 0 if total_sent_7day == 0 else int(total_fees_7day/(total_sent_7day/1000000)) + total_fees_ppm_1day = 0 if total_sent_1day == 0 else int(total_fees_1day/(total_sent_1day/1000000)) + onchain_costs = 0 if onchain_txs.count() == 0 else onchain_txs.aggregate(Sum('fee'))['fee__sum'] + onchain_costs_90day = 0 if onchain_txs_90day.count() == 0 else onchain_txs_90day.aggregate(Sum('fee'))['fee__sum'] + onchain_costs_30day = 0 if onchain_txs_30day.count() == 0 else onchain_txs_30day.aggregate(Sum('fee'))['fee__sum'] + onchain_costs_7day = 0 if onchain_txs_7day.count() == 0 else onchain_txs_7day.aggregate(Sum('fee'))['fee__sum'] + onchain_costs_1day = 0 if onchain_txs_1day.count() == 0 else onchain_txs_1day.aggregate(Sum('fee'))['fee__sum'] + close_fees = channels.filter(chan_id__in=closures.values('chan_id')).aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures.exists() else 0 + close_fees_90day = channels.filter(chan_id__in=closures_90day.values('chan_id')).aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_90day.exists() else 0 + close_fees_30day = channels.filter(chan_id__in=closures_30day.values('chan_id')).aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_30day.exists() else 0 + close_fees_7day = channels.filter(chan_id__in=closures_7day.values('chan_id')).aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_7day.exists() else 0 + close_fees_1day = channels.filter(chan_id__in=closures_1day.values('chan_id')).aggregate(Sum('closing_costs'))['closing_costs__sum'] if closures_1day.exists() else 0 + onchain_costs += close_fees + onchain_costs_90day += close_fees_90day + onchain_costs_30day += close_fees_30day + onchain_costs_7day += close_fees_7day + onchain_costs_1day += close_fees_1day + profits = int(total_revenue-total_fees-onchain_costs) + profits_90day = int(total_revenue_90day-total_fees_90day-onchain_costs_90day) + profits_30day = int(total_revenue_30day-total_fees_30day-onchain_costs_30day) + profits_7day = int(total_revenue_7day-total_fees_7day-onchain_costs_7day) + profits_1day = int(total_revenue_1day-total_fees_1day-onchain_costs_1day) + context = { + 'node_info': node_info, + 'forward_count': forward_count, + 'forward_count_90day': forward_count_90day, + 'forward_count_30day': forward_count_30day, + 'forward_count_7day': forward_count_7day, + 'forward_count_1day': forward_count_1day, + 'forward_amount': forward_amount, + 'forward_amount_90day': forward_amount_90day, + 'forward_amount_30day': forward_amount_30day, + 'forward_amount_7day': forward_amount_7day, + 'forward_amount_1day': forward_amount_1day, + 'total_revenue': total_revenue, + 'total_revenue_90day': total_revenue_90day, + 'total_revenue_30day': total_revenue_30day, + 'total_revenue_7day': total_revenue_7day, + 'total_revenue_1day': total_revenue_1day, + 'total_fees': total_fees, + 'total_fees_90day': total_fees_90day, + 'total_fees_30day': total_fees_30day, + 'total_fees_7day': total_fees_7day, + 'total_fees_1day': total_fees_1day, + 'total_fees_ppm': total_fees_ppm, + 'total_fees_ppm_90day': total_fees_ppm_90day, + 'total_fees_ppm_30day': total_fees_ppm_30day, + 'total_fees_ppm_7day': total_fees_ppm_7day, + 'total_fees_ppm_1day': total_fees_ppm_1day, + 'onchain_costs': onchain_costs, + 'onchain_costs_90day': onchain_costs_90day, + 'onchain_costs_30day': onchain_costs_30day, + 'onchain_costs_7day': onchain_costs_7day, + 'onchain_costs_1day': onchain_costs_1day, + 'total_revenue_ppm': total_revenue_ppm, + 'total_revenue_ppm_90day': total_revenue_ppm_90day, + 'total_revenue_ppm_30day': total_revenue_ppm_30day, + 'total_revenue_ppm_7day': total_revenue_ppm_7day, + 'total_revenue_ppm_1day': total_revenue_ppm_1day, + 'profits': profits, + 'profits_90day': profits_90day, + 'profits_30day': profits_30day, + 'profits_7day': profits_7day, + 'profits_1day': profits_1day, + 'profits_ppm': 0 if forward_amount == 0 else int(profits/(forward_amount/1000000)), + 'profits_ppm_90day': 0 if forward_amount_90day == 0 else int(profits_90day/(forward_amount_90day/1000000)), + 'profits_ppm_30day': 0 if forward_amount_30day == 0 else int(profits_30day/(forward_amount_30day/1000000)), + 'profits_ppm_7day': 0 if forward_amount_7day == 0 else int(profits_7day/(forward_amount_7day/1000000)), + 'profits_ppm_1day': 0 if forward_amount_1day == 0 else int(profits_1day/(forward_amount_1day/1000000)), + 'percent_cost': 0 if total_revenue == 0 else int(((total_fees+onchain_costs)/total_revenue)*100), + 'percent_cost_90day': 0 if total_revenue_90day == 0 else int(((total_fees_90day+onchain_costs_90day)/total_revenue_90day)*100), + 'percent_cost_30day': 0 if total_revenue_30day == 0 else int(((total_fees_30day+onchain_costs_30day)/total_revenue_30day)*100), + 'percent_cost_7day': 0 if total_revenue_7day == 0 else int(((total_fees_7day+onchain_costs_7day)/total_revenue_7day)*100), + 'percent_cost_1day': 0 if total_revenue_1day == 0 else int(((total_fees_1day+onchain_costs_1day)/total_revenue_1day)*100), + 'network': 'testnet/' if 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=/') def channel(request): if request.method == 'GET': @@ -638,8 +892,12 @@ def channel(request): payments_df = DataFrame.from_records(Payments.objects.filter(status=2).filter(chan_out=chan_id).filter(rebal_chan__isnull=False).annotate(ppm=Round((Sum('fee')*1000000)/Sum('value'), output_field=IntegerField())).values()) invoices_df = DataFrame.from_records(Invoices.objects.filter(state=1).filter(chan_in=chan_id).filter(r_hash__in=Payments.objects.filter(status=2).filter(rebal_chan=chan_id)).values()) channels_df = DataFrame.from_records(Channels.objects.filter(is_open=True).values()) - node_outbound = channels_df['local_balance'].sum() - node_capacity = channels_df['capacity'].sum() + if channels_df.empty: + node_outbound = 0 + node_capacity = 0 + else: + node_outbound = channels_df['local_balance'].sum() + node_capacity = channels_df['capacity'].sum() 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()) @@ -887,6 +1145,7 @@ def channel(request): 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['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'] @@ -1126,6 +1385,16 @@ def invoices(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') +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) + else: + return redirect('home') + @login_required(login_url='/lndg-admin/login/?next=/') def batch(request): if request.method == 'GET': @@ -1302,12 +1571,16 @@ def rebalancing(request): eligible_df = enabled_df[enabled_df['is_active']==True][enabled_df['inbound_can']>=1][enabled_df['fee_check']<100] eligible_count = eligible_df.shape[0] enabled_count = enabled_df.shape[0] + available_df = channels_df[channels_df['auto_rebalance']==False][channels_df['is_active']==True][channels_df['percent_outbound'] / channels_df['ar_out_target']>=1] + available_count = available_df.shape[0] else: eligible_count = 0 enabled_count = 0 + available_count = 0 context = { 'eligible_count': eligible_count, 'enabled_count': enabled_count, + 'available_count': available_count, 'channels': channels_df.to_dict(orient='records'), 'rebalancer': Rebalancer.objects.all().annotate(ppm=Round((Sum('fee_limit')*1000000)/Sum('value'), output_field=IntegerField())).order_by('-id')[:20], 'rebalancer_form': RebalancerForm, @@ -1593,8 +1866,11 @@ def auto_rebalance(request): db_percent_target = LocalSettings.objects.get(key='AR-Target%') db_percent_target.value = target_percent db_percent_target.save() - Channels.objects.all().update(ar_amt_target=Round(F('capacity')*(target_percent/100), output_field=IntegerField())) - messages.success(request, 'Updated auto rebalancer target amount for all channels to: ' + str(target_percent)) + if form.cleaned_data['targetallchannels']: + Channels.objects.all().update(ar_amt_target=Round(F('capacity')*(target_percent/100), output_field=IntegerField())) + messages.success(request, 'Updated auto rebalancer target amount for all channels to: ' + str(target_percent)) + else: + messages.success(request, 'Updated auto rebalancer target amount in local settings: ' + str(target_percent)) if form.cleaned_data['target_time'] is not None: target_time = form.cleaned_data['target_time'] try: @@ -1624,8 +1900,25 @@ def auto_rebalance(request): db_outbound_target = LocalSettings.objects.get(key='AR-Outbound%') db_outbound_target.value = outbound_percent db_outbound_target.save() - Channels.objects.all().update(ar_out_target=int(outbound_percent)) - messages.success(request, 'Updated auto rebalancer target outbound percent setting for all channels to: ' + str(outbound_percent)) + if form.cleaned_data['targetallchannels']: + Channels.objects.all().update(ar_out_target=int(outbound_percent)) + messages.success(request, 'Updated auto rebalancer target outbound percent setting for all channels to: ' + str(outbound_percent)) + else: + messages.success(request, 'Updated auto rebalancer target outbound percent setting in local settings to: ' + str(outbound_percent)) + if form.cleaned_data['inbound_percent'] is not None: + inbound_percent = int(form.cleaned_data['inbound_percent']) + try: + db_inbound_target = LocalSettings.objects.get(key='AR-Inbound%') + except: + LocalSettings(key='AR-Inbound%', value='100').save() + db_inbound_target = LocalSettings.objects.get(key='AR-Inbound%') + db_inbound_target.value = inbound_percent + db_inbound_target.save() + if form.cleaned_data['targetallchannels']: + Channels.objects.all().update(ar_out_target=int(outbound_percent)) + messages.success(request, 'Updated auto rebalancer target inbound percent setting for all channels to: ' + str(inbound_percent)) + else: + messages.success(request, 'Updated auto rebalancer target inbound percent setting in local settigs to: ' + str(inbound_percent)) if form.cleaned_data['fee_rate'] is not None: fee_rate = form.cleaned_data['fee_rate'] try: @@ -1645,8 +1938,11 @@ def auto_rebalance(request): db_max_cost = LocalSettings.objects.get(key='AR-MaxCost%') db_max_cost.value = max_cost db_max_cost.save() - Channels.objects.all().update(ar_max_cost=int(max_cost)) - messages.success(request, 'Updated auto rebalancer max cost setting to: ' + str(max_cost)) + if form.cleaned_data['targetallchannels']: + Channels.objects.all().update(ar_max_cost=int(max_cost)) + messages.success(request, 'Updated auto rebalancer max cost setting for all channels to: ' + str(max_cost)) + else: + messages.success(request, 'Updated auto rebalancer max cost setting in local settings to: ' + str(max_cost)) if form.cleaned_data['autopilot'] is not None: autopilot = form.cleaned_data['autopilot'] try: @@ -1657,6 +1953,16 @@ def auto_rebalance(request): db_autopilot.value = autopilot db_autopilot.save() messages.success(request, 'Updated autopilot setting to: ' + str(autopilot)) + if form.cleaned_data['autopilotdays'] is not None: + autopilotdays = form.cleaned_data['autopilotdays'] + try: + db_autopilotdays = LocalSettings.objects.get(key='AR-APDays') + except: + LocalSettings(key='AR-APDays', value='7').save() + db_autopilotdays = LocalSettings.objects.get(key='AR-APDays') + db_autopilotdays.value = autopilotdays + db_autopilotdays.save() + messages.success(request, 'Updated autopilot days setting to: ' + str(autopilotdays)) if form.cleaned_data['variance'] is not None: variance = form.cleaned_data['variance'] try: @@ -1757,6 +2063,66 @@ def update_channel(request): 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)) + elif update_target == 10: + db_channel.closing_costs = target + 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)) + 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=/') +def update_pending(request): + if request.method == 'POST': + form = UpdatePending(request.POST) + if form.is_valid(): + funding_txid = form.cleaned_data['funding_txid'] + output_index = form.cleaned_data['output_index'] + target = form.cleaned_data['target'] + update_target = int(form.cleaned_data['update_target']) + if PendingChannels.objects.filter(funding_txid=funding_txid, output_index=output_index).exists(): + pending_channel = PendingChannels.objects.filter(funding_txid=funding_txid, output_index=output_index)[0] + else: + pending_channel = PendingChannels(funding_txid=funding_txid, output_index=output_index) + pending_channel.save() + if update_target == 0: + pending_channel.local_base_fee = target + pending_channel.save() + messages.success(request, 'Base fee for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target)) + elif update_target == 1: + pending_channel.local_fee_rate = target + pending_channel.save() + messages.success(request, 'Fee rate for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target)) + elif update_target == 2: + pending_channel.ar_amt_target = target + pending_channel.save() + messages.success(request, 'Auto rebalancer target amount for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target)) + elif update_target == 3: + pending_channel.ar_in_target = target + pending_channel.save() + messages.success(request, 'Auto rebalancer inbound target for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target) + '%') + elif update_target == 4: + pending_channel.ar_out_target = target + pending_channel.save() + messages.success(request, 'Auto rebalancer outbound target for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target) + '%') + elif update_target == 5: + pending_channel.auto_rebalance = True if pending_channel.auto_rebalance == False else False + pending_channel.save() + messages.success(request, 'Auto rebalancer status for pending pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(pending_channel.auto_rebalance)) + elif update_target == 6: + pending_channel.ar_max_cost = target + pending_channel.save() + messages.success(request, 'Auto rebalancer max cost for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target) + '%') + elif update_target == 8: + pending_channel.auto_fees = True if pending_channel.auto_fees == False else False + pending_channel.save() + messages.success(request, 'Auto fees status for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(pending_channel.auto_fees)) + elif update_target == 9: + pending_channel.local_cltv = target + pending_channel.save() + messages.success(request, 'CLTV for pending channel (' + str(funding_txid) + ') updated to a value of: ' + str(target)) else: messages.error(request, 'Invalid target code. Please try again.') else: @@ -1779,7 +2145,7 @@ def update_setting(request): db_percent_target = LocalSettings.objects.get(key='AR-Target%') db_percent_target.value = target_percent db_percent_target.save() - messages.success(request, 'Updated auto rebalancer target amount for all channels to: ' + str(target_percent)) + messages.success(request, 'Updated auto rebalancer target amount to: ' + str(target_percent)) elif key == 'AR-Time': target_time = int(value) try: @@ -1809,7 +2175,17 @@ def update_setting(request): db_outbound_target = LocalSettings.objects.get(key='AR-Outbound%') db_outbound_target.value = outbound_percent db_outbound_target.save() - messages.success(request, 'Updated auto rebalancer target outbound percent setting for all channels to: ' + str(outbound_percent)) + messages.success(request, 'Updated auto rebalancer target outbound percent setting: ' + str(outbound_percent)) + elif key == 'AR-Inbound%': + inbound_percent = int(value) + try: + db_inbound_target = LocalSettings.objects.get(key='AR-Inbound%') + except: + LocalSettings(key='AR-Inbound%', value='100').save() + db_inbound_target = LocalSettings.objects.get(key='AR-Inbound%') + db_inbound_target.value = inbound_percent + db_inbound_target.save() + messages.success(request, 'Updated auto rebalancer target inbound percent setting: ' + str(inbound_percent)) elif key == 'AR-MaxFeeRate': fee_rate = int(value) try: @@ -1840,6 +2216,16 @@ def update_setting(request): db_autopilot.value = autopilot db_autopilot.save() messages.success(request, 'Updated autopilot setting to: ' + str(autopilot)) + elif key == 'AR-APDays': + apdays = int(value) + try: + db_apdays = LocalSettings.objects.get(key='AR-APDays') + except: + LocalSettings(key='AR-APDays', value='7').save() + db_apdays = LocalSettings.objects.get(key='AR-APDays') + db_apdays.value = apdays + db_apdays.save() + messages.success(request, 'Updated Autopilot Days setting to: ' + str(apdays)) elif key == 'AR-Variance': variance = int(value) try: @@ -2327,4 +2713,4 @@ def pending_channels(request): details_index = error.find('details =') + 11 debug_error_index = error.find('debug_error_string =') - 3 error_msg = error[details_index:debug_error_index] - return Response({'error': 'Failed to get pending channels! Error: ' + error_msg}) \ No newline at end of file + return Response({'error': 'Failed to get pending channels! Error: ' + error_msg}) diff --git a/initialize.py b/initialize.py index fab5f7c..b2232b8 100644 --- a/initialize.py +++ b/initialize.py @@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model 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): +def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenoise, debug, csrftrusted): #Generate a unique secret to be used for your django site secret = secrets.token_urlsafe(64) if whitenoise: @@ -13,6 +13,12 @@ def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenois 'whitenoise.middleware.WhiteNoiseMiddleware',""" else: wnl = '' + if csrftrusted: + csrf = """ +CSRF_TRUSTED_ORIGINS = [%s] + """ % (csrftrusted) + else: + csrf = '' settings_file = '''""" Django settings for lndg project. @@ -41,7 +47,7 @@ SECRET_KEY = '%s' DEBUG = %s ALLOWED_HOSTS = ['%s'] - +%s LND_DIR_PATH = '%s' LND_NETWORK = '%s' LND_RPC_SERVER = '%s' @@ -151,7 +157,7 @@ USE_TZ = False STATIC_URL = 'static/' STATIC_ROOT = os.path.join(BASE_DIR, 'gui/static/') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -''' % (secret, debug, node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, wnl) +''' % (secret, debug, node_ip, csrf, lnd_dir_path, lnd_network, lnd_rpc_server, wnl) try: f = open("lndg/settings.py", "x") f.close() @@ -247,7 +253,9 @@ def main(): parser.add_argument('-wn', '--whitenoise', help = 'Add whitenoise middleware (docker requirement for static files)', action='store_true') parser.add_argument('-d', '--docker', help = 'Single option for docker container setup (supervisord + whitenoise)', action='store_true') parser.add_argument('-dx', '--debug', help = 'Setup the django site in debug mode', action='store_true') + 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) args = parser.parse_args() node_ip = args.nodeip lnd_dir_path = args.lnddir @@ -258,11 +266,13 @@ def main(): whitenoise = args.whitenoise docker = args.docker debug = args.debug + adminuser = args.adminuser adminpw = args.adminpw + csrftrusted = args.csrftrusted if docker: setup_supervisord = True whitenoise = True - write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenoise, debug) + write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenoise, debug, csrftrusted) if setup_supervisord: print('Supervisord setup requested...') write_supervisord_settings(sduser) @@ -300,8 +310,8 @@ def main(): if get_user_model().objects.count() == 0: print('Setting up initial user...') try: - call_command('createsuperuser', username='lndg-admin', email='admin@lndg.local', interactive=False) - admin = get_user_model().objects.get(username='lndg-admin') + call_command('createsuperuser', username=adminuser, email='admin@lndg.local', interactive=False) + admin = get_user_model().objects.get(username=adminuser) login_pw = secrets.token_urlsafe(16) if adminpw is None else adminpw admin.set_password(login_pw) admin.save() diff --git a/jobs.py b/jobs.py index 2a1dc7e..987099f 100644 --- a/jobs.py +++ b/jobs.py @@ -1,5 +1,5 @@ import django -from django.db.models import Max +from django.db.models import Max, Min from datetime import datetime, timedelta from gui.lnd_deps import lightning_pb2 as ln from gui.lnd_deps import lightning_pb2_grpc as lnrpc @@ -9,17 +9,24 @@ from gui.lnd_deps.lnd_connect import lnd_connect from lndg import settings from os import environ from pandas import DataFrame +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 +from gui.models import Payments, PaymentHops, Invoices, Forwards, Channels, Peers, Onchain, Closures, Resolutions, PendingHTLCs, LocalSettings, FailedHTLCs, Autofees, PendingChannels +from lndg.settings import LND_NETWORK def update_payments(stub): - #Remove anything in-flight so we can get most up to date status - Payments.objects.filter(status=1).delete() - #Get the number of records in the database currently - last_index = 0 if Payments.objects.aggregate(Max('index'))['index__max'] == None else Payments.objects.aggregate(Max('index'))['index__max'] - payments = stub.ListPayments(ln.ListPaymentsRequest(include_incomplete=True, index_offset=last_index, max_payments=100)).payments 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: + update_payment(stub, payment_data[0], self_pubkey) + else: + payment.status = 3 + payment.save() + 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: 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) @@ -57,85 +64,108 @@ def update_payments(stub): new_payment.save() except: #Error inserting, try to update instead - db_payment = Payments.objects.filter(payment_hash=payment.payment_hash)[0] - db_payment.creation_date = datetime.fromtimestamp(payment.creation_date) - db_payment.value = round(payment.value_msat/1000, 3) - 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: - for attempt in payment.htlcs: - if attempt.status == 1: - PaymentHops.objects.filter(payment_hash=db_payment).delete() - 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=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 db_payment.chan_out is None: - db_payment.chan_out = hop.chan_id - db_payment.chan_out_alias = alias - else: - db_payment.chan_out = 'MPP' - db_payment.chan_out_alias = 'MPP' - if hop_count == total_hops and 5482373484 in hop.custom_records and db_payment.keysend_preimage is None: - records = hop.custom_records - message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None - db_payment.keysend_preimage = records[5482373484].hex() - 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() + update_payment(stub, payment, self_pubkey) + +def update_payment(stub, payment, self_pubkey): + db_payment = Payments.objects.filter(payment_hash=payment.payment_hash)[0] + db_payment.creation_date = datetime.fromtimestamp(payment.creation_date) + db_payment.value = round(payment.value_msat/1000, 3) + 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: + PaymentHops.objects.filter(payment_hash=db_payment).delete() + 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=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 db_payment.chan_out is None: + db_payment.chan_out = hop.chan_id + db_payment.chan_out_alias = alias + else: + db_payment.chan_out = 'MPP' + db_payment.chan_out_alias = 'MPP' + if hop_count == total_hops and 5482373484 in hop.custom_records and db_payment.keysend_preimage is None: + records = hop.custom_records + message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None + db_payment.keysend_preimage = records[5482373484].hex() + 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() def update_invoices(stub): - #Remove anything open so we can get most up to date status - Invoices.objects.filter(state=0).delete() - last_index = 0 if Invoices.objects.aggregate(Max('index'))['index__max'] == None else Invoices.objects.aggregate(Max('index'))['index__max'] + open_invoices = Invoices.objects.filter(state=0).order_by('index') + for open_invoice in open_invoices: + 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) + else: + open_invoice.state = 2 + open_invoice.save() + last_index = Invoices.objects.aggregate(Max('index'))['index__max'] if Invoices.objects.exists() else 0 invoices = stub.ListInvoices(ln.ListInvoiceRequest(index_offset=last_index, num_max_invoices=100)).invoices for invoice in invoices: - if invoice.state == 1: - if len(invoice.htlcs) > 0: - chan_in_id = invoice.htlcs[0].chan_id - alias = Channels.objects.filter(chan_id=chan_in_id)[0].alias if Channels.objects.filter(chan_id=chan_in_id).exists() else None - records = invoice.htlcs[0].custom_records - 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)) - 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 - except: - print('Unable to validate signature on invoice: ' + invoice.r_hash.hex()) - valid = False - sender = records[34349339].hex() if valid == True else None - try: - sender_alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=sender, include_channels=False)).node.alias if sender != None else None - except: - sender_alias = None - else: - sender = None + db_invoice = Invoices(creation_date=datetime.fromtimestamp(invoice.creation_date), r_hash=invoice.r_hash.hex(), value=round(invoice.value_msat/1000, 3), amt_paid=invoice.amt_paid_sat, state=invoice.state, index=invoice.add_index) + db_invoice.save() + update_invoice(stub, invoice, db_invoice) + +def update_invoice(stub, invoice, db_invoice): + if invoice.state == 1: + if len(invoice.htlcs) > 0: + chan_in_id = invoice.htlcs[0].chan_id + alias = Channels.objects.filter(chan_id=chan_in_id)[0].alias if Channels.objects.filter(chan_id=chan_in_id).exists() else None + records = invoice.htlcs[0].custom_records + 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)) + 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 + except: + print('Unable to validate signature on invoice: ' + invoice.r_hash.hex()) + valid = False + sender = records[34349339].hex() if valid == True else None + try: + sender_alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=sender, include_channels=False)).node.alias if sender != None else None + except: sender_alias = None else: - chan_in_id = None - alias = None - keysend_preimage = None - message = None sender = None sender_alias = None - Invoices(creation_date=datetime.fromtimestamp(invoice.creation_date), settle_date=datetime.fromtimestamp(invoice.settle_date), r_hash=invoice.r_hash.hex(), value=round(invoice.value_msat/1000, 3), amt_paid=invoice.amt_paid_sat, state=invoice.state, chan_in=chan_in_id, chan_in_alias=alias, keysend_preimage=keysend_preimage, message=message, sender=sender, sender_alias=sender_alias, index=invoice.add_index).save() else: - Invoices(creation_date=datetime.fromtimestamp(invoice.creation_date), r_hash=invoice.r_hash.hex(), value=round(invoice.value_msat/1000, 3), amt_paid=invoice.amt_paid_sat, state=invoice.state, index=invoice.add_index).save() + chan_in_id = None + alias = None + keysend_preimage = None + message = None + sender = None + sender_alias = None + db_invoice.state = invoice.state + db_invoice.amt_paid = invoice.amt_paid_sat + db_invoice.settle_date = datetime.fromtimestamp(invoice.settle_date) + db_invoice.chan_in = chan_in_id + db_invoice.chan_in_alias = alias + db_invoice.keysend_preimage = keysend_preimage + db_invoice.message = message + db_invoice.sender = sender + db_invoice.sender_alias = sender_alias + else: + db_invoice.state = invoice.state + db_invoice.save() def update_forwards(stub): records = Forwards.objects.count() @@ -156,6 +186,7 @@ def update_channels(stub): if Channels.objects.filter(chan_id=channel.chan_id).exists(): #Update the channel record with the most current data db_channel = Channels.objects.filter(chan_id=channel.chan_id)[0] + pending_channel = None else: #Create a record for this new channel try: @@ -173,6 +204,7 @@ def update_channels(stub): db_channel.output_index = index db_channel.capacity = channel.capacity db_channel.private = channel.private + 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)) if chan_data.node1_pub == channel.remote_pubkey: @@ -239,6 +271,33 @@ def update_channels(stub): db_channel.pending_outbound = pending_out db_channel.pending_inbound = pending_in db_channel.htlc_count = htlc_counter + if pending_channel: + if pending_channel.local_base_fee or pending_channel.local_fee_rate or pending_channel.local_cltv: + base_fee = pending_channel.local_base_fee if pending_channel.local_base_fee else db_channel.local_base_fee + fee_rate = pending_channel.local_fee_rate if pending_channel.local_fee_rate else db_channel.local_fee_rate + cltv = pending_channel.local_cltv if pending_channel.local_cltv else db_channel.local_cltv + 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 = int(db_channel.output_index) + stub.UpdateChannelPolicy(ln.PolicyUpdateRequest(chan_point=channel_point, base_fee_msat=base_fee, fee_rate=(fee_rate/1000000), time_lock_delta=cltv)) + db_channel.local_base_fee = base_fee + db_channel.local_fee_rate = fee_rate + db_channel.local_cltv = cltv + db_channel.fees_updated = datetime.now() + if pending_channel.auto_rebalance: + db_channel.auto_rebalance = pending_channel.auto_rebalance + if pending_channel.ar_amt_target: + db_channel.ar_amt_target = pending_channel.ar_amt_target + if pending_channel.ar_in_target: + db_channel.ar_in_target = pending_channel.ar_in_target + if pending_channel.ar_out_target: + 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: + db_channel.auto_fees = pending_channel.auto_fees + pending_channel.delete() db_channel.save() counter += 1 chan_list.append(channel.chan_id) @@ -293,6 +352,24 @@ def update_onchain(stub): for tx in onchain_txs: Onchain(tx_hash=tx.tx_hash, time_stamp=datetime.fromtimestamp(tx.time_stamp), amount=tx.amount, fee=tx.total_fees, block_hash=tx.block_hash, block_height=tx.block_height, label=tx.label[:100]).save() +def network_links(): + if LocalSettings.objects.filter(key='GUI-NetLinks').exists(): + network_links = str(LocalSettings.objects.filter(key='GUI-NetLinks')[0].value) + else: + LocalSettings(key='GUI-NetLinks', value='https://mempool.space').save() + network_links = 'https://mempool.space' + return network_links + +def get_tx_fees(txid): + base_url = network_links() + ('/testnet' if 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)) + fee = 0 + return fee + def update_closures(stub): closures = stub.ClosedChannels(ln.ClosedChannelsRequest()).channels if len(closures) > Closures.objects.all().count(): @@ -301,8 +378,10 @@ def update_closures(stub): for closure in closures: counter += 1 if counter > skip: + 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 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() @@ -313,7 +392,12 @@ def update_closures(stub): if resolution_count > 0: Resolutions.objects.filter(chan_id=closure.chan_id).delete() for resolution in closure.resolutions: + 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() def reconnect_peers(stub): inactive_peers = Channels.objects.filter(is_open=True, is_active=False, private=False).values_list('remote_pubkey', flat=True).distinct() @@ -336,9 +420,16 @@ def reconnect_peers(stub): print('Unable to find node info on graph, using last known value') host = peer.address address = ln.LightningAddress(pubkey=inactive_peer, host=host) - stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=address, perm=True, timeout=5)) - peer.last_reconnected = datetime.now() - peer.save() + try: + stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=address, perm=True, timeout=5)) + peer.last_reconnected = datetime.now() + peer.save() + 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] + print (f"{datetime.now().strftime('%c')} : Error reconnecting {inactive_peer=} {error_msg=}") def clean_payments(stub): if LocalSettings.objects.filter(key='LND-CleanPayments').exists(): @@ -365,11 +456,11 @@ def clean_payments(stub): details_index = error.find('details =') + 11 debug_error_index = error.find('debug_error_string =') - 3 error_msg = error[details_index:debug_error_index] - print('Error occured when cleaning payment: ' + payment.payment_hash) - print('Error: ' + error_msg) + print (f"{datetime.now().strftime('%c')} : Error {payment.index=} {payment.status=} {payment.payment_hash=} {error_msg=}") finally: payment.cleaned = True payment.save() + 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(): diff --git a/nginx.sh b/nginx.sh index 73186b9..cf158be 100644 --- a/nginx.sh +++ b/nginx.sh @@ -87,6 +87,8 @@ EOF function setup_nginx() { cat << EOF > /etc/nginx/sites-enabled/lndg +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 04ee4e6..3648fc1 100644 --- a/rebalancer.py +++ b/rebalancer.py @@ -21,7 +21,8 @@ def run_rebalancer(rebalance): unknown_error.save() 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')) 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(outbound_cans) == 0: + if len(outbound_cans) == 0 and rebalance.manual == False: + print ('No outbound_cans') return None elif str(outbound_cans).replace('\'', '') != rebalance.outgoing_chan_ids and rebalance.manual == False: rebalance.outgoing_chan_ids = str(outbound_cans).replace('\'', '') @@ -34,7 +35,9 @@ def run_rebalancer(rebalance): chan_ids = json.loads(rebalance.outgoing_chan_ids) timeout = rebalance.duration * 60 invoice_response = stub.AddInvoice(ln.Invoice(value=rebalance.value, expiry=timeout)) + #print('Rebalance for:', rebalance.target_alias, ' : ', rebalance.last_hop_pubkey, ' Amount:', rebalance.value, ' Duration:', rebalance.duration, ' via:', chan_ids ) for payment_response in routerstub.SendPaymentV2(lnr.SendPaymentRequest(payment_request=str(invoice_response.payment_request), fee_limit_msat=int(rebalance.fee_limit*1000), outgoing_chan_ids=chan_ids, last_hop_pubkey=bytes.fromhex(rebalance.last_hop_pubkey), timeout_seconds=(timeout-5), allow_self_payment=True), timeout=(timeout+60)): + #print ('Payment Response:', payment_response.status, ' Reason:', payment_response.failure_reason, 'Payment Hash :', payment_response.payment_hash ) if payment_response.status == 1 and rebalance.status == 0: #IN-FLIGHT rebalance.payment_hash = payment_response.payment_hash @@ -43,6 +46,7 @@ def run_rebalancer(rebalance): elif payment_response.status == 2: #SUCCESSFUL rebalance.status = 2 + rebalance.fees_paid = payment_response.fee_msat/1000 successful_out = payment_response.htlcs[0].route.hops[0].pub_key elif payment_response.status == 3: #FAILURE @@ -64,6 +68,7 @@ def run_rebalancer(rebalance): elif payment_response.status == 0: rebalance.status = 400 except Exception as e: + #print('Exception: ', str(e)) if str(e.code()) == 'StatusCode.DEADLINE_EXCEEDED': rebalance.status = 408 else: @@ -113,6 +118,8 @@ def auto_schedule(): if len(auto_rebalance_channels) > 0: if not LocalSettings.objects.filter(key='AR-Outbound%').exists(): LocalSettings(key='AR-Outbound%', value='75').save() + 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) if len(inbound_cans) > 0 and len(outbound_cans) > 0: @@ -165,46 +172,61 @@ def auto_enable(): else: LocalSettings(key='AR-Autopilot', value='0').save() enabled = 0 + if LocalSettings.objects.filter(key='AR-APDays').exists(): + apdays = int(LocalSettings.objects.filter(key='AR-APDays')[0].value) + else: + LocalSettings(key='AR-APDays', value='7').save() + apdays = 7 if enabled == 1: - 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')) - filter_7day = datetime.now() - timedelta(days=7) - forwards = Forwards.objects.filter(forward_date__gte=filter_7day) + lookup_channels=Channels.objects.filter(is_active=True, is_open=True, private=False) + channels = lookup_channels.values('remote_pubkey').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() + filter_day = datetime.now() - timedelta(days=apdays) + forwards = Forwards.objects.filter(forward_date__gte=filter_day) for channel in channels: - outbound_percent = int(round(channel.outbound_percent/10, 0)) - inbound_percent = int(round(channel.inbound_percent/10, 0)) - routed_in_7day = forwards.filter(chan_id_in=channel.chan_id).count() - routed_out_7day = forwards.filter(chan_id_out=channel.chan_id).count() - i7D = 0 if routed_in_7day == 0 else int(forwards.filter(chan_id_in=channel.chan_id).aggregate(Sum('amt_in_msat'))['amt_in_msat__sum']/10000000)/100 - o7D = 0 if routed_out_7day == 0 else int(forwards.filter(chan_id_out=channel.chan_id).aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/10000000)/100 - if o7D > (i7D*1.10) and outbound_percent > 75: - #print('Case 1: Pass') - pass - elif o7D > (i7D*1.10) and inbound_percent > 75 and channel.auto_rebalance == False: - #print('Case 2: Enable AR - o7D > i7D AND Inbound Liq > 75%') - channel.auto_rebalance = True - channel.save() - Autopilot(chan_id=channel.chan_id, peer_alias=channel.alias, setting='Enabled', old_value=0, new_value=1).save() - elif o7D < (i7D*1.10) and outbound_percent > 75 and channel.auto_rebalance == True: - #print('Case 3: Disable AR - o7D < i7D AND Outbound Liq > 75%') - channel.auto_rebalance = False - channel.save() - Autopilot(chan_id=channel.chan_id, peer_alias=channel.alias, setting='Enabled', old_value=1, new_value=0).save() - elif o7D < (i7D*1.10) and inbound_percent > 75: - #print('Case 4: Pass') - pass - else: - #print('Case 5: Pass') - pass + outbound_percent = int(round(channel['outbound_percent']/10, 0)) + inbound_percent = int(round(channel['inbound_percent']/10, 0)) + chan_list = lookup_channels.filter(remote_pubkey=channel['remote_pubkey']).values('chan_id') + routed_in_apday = forwards.filter(chan_id_in__in=chan_list).count() + routed_out_apday = forwards.filter(chan_id_out__in=chan_list).count() + iapD = 0 if routed_in_apday == 0 else int(forwards.filter(chan_id_in__in=chan_list).aggregate(Sum('amt_in_msat'))['amt_in_msat__sum']/10000000)/100 + oapD = 0 if routed_out_apday == 0 else int(forwards.filter(chan_id_out__in=chan_list).aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/10000000)/100 + + 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: + #print('Case 1: Pass') + pass + elif oapD > (iapD*1.10) and inbound_percent > 75 and peer_channel.auto_rebalance == False: + #print('Case 2: Enable AR - o7D > i7D AND Inbound Liq > 75%') + peer_channel.auto_rebalance = True + peer_channel.save() + Autopilot(chan_id=peer_channel.chan_id, peer_alias=peer_channel.alias, setting='Enabled', old_value=0, new_value=1).save() + print('Auto Pilot Enabled: ', peer_channel.alias, ' : ', peer_channel.chan_id , ' Out: ', oapD, ' In: ', iapD) + elif oapD < (iapD*1.10) and outbound_percent > 75 and peer_channel.auto_rebalance == True: + #print('Case 3: Disable AR - o7D < i7D AND Outbound Liq > 75%') + peer_channel.auto_rebalance = False + peer_channel.save() + Autopilot(chan_id=peer_channel.chan_id, peer_alias=peer_channel.alias, setting='Enabled', old_value=1, new_value=0).save() + print('Auto Pilot Disabled (3): ', peer_channel.alias, ' : ', peer_channel.chan_id, ' Out: ', oapD, ' In: ', iapD ) + elif oapD < (iapD*1.10) and inbound_percent > 75: + #print('Case 4: Pass') + pass + else: + #print('Case 5: Pass') + pass def main(): + rebalances = Rebalancer.objects.filter(status=0).order_by('id') if len(rebalances) == 0: auto_enable() auto_schedule() else: rebalance = rebalances[0] + #print('Next Rebalance for:', rebalance.target_alias, ' : ', rebalance.last_hop_pubkey, ' Amount:', rebalance.value, ' Duration:', rebalance.duration ) while rebalance != None: rebalance = run_rebalancer(rebalance) if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/requirements.txt b/requirements.txt index d8d7fb4..a197baf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ django-qr-code grpcio protobuf pytz -pandas \ No newline at end of file +pandas +requests \ No newline at end of file
StepFee PPM Cost To📍 Alias Channel ID Channel Capacity{{ hop.fee|intcomma }} {{ hop.ppm|intcomma }} {{ hop.cost_to|intcomma }}{% if hop.step == 1 %}📍{% else %}🔻{% endif %} {% if hop.alias == '' %}---{% else %}{{ hop.alias }}{% endif %}{{ hop.chan_id }}{{ hop.chan_id }} {{ hop.chan_capacity|intcomma }}