This commit is contained in:
cryptosharks131 2022-08-26 20:56:46 -04:00 committed by GitHub
parent 53e58586f3
commit c63c065c6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1382 additions and 330 deletions

46
.github/workflows/on-push-github.yml vendored Normal file
View file

@ -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" ./

47
.github/workflows/on-tag-github.yml vendored Normal file
View file

@ -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" ./

View file

@ -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`

View file

@ -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')
fee_rate = forms.IntegerField(label='fee_rate')

View file

@ -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),
]

View file

@ -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')},
},
),
]

View file

@ -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'
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'),)

View file

@ -204,11 +204,11 @@
{% csrf_token %}
{% if settings.key == 'AR-Target%' %}
<input style="text-align:center" id="value" type="number" step="0.1" min="0.1" max="100" name="value" value="{{ settings.value }}">
{% 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' %}
<input style="text-align:center" id="value" type="number" min="1" max="100" name="value" value="{{ settings.value }}">
{% elif settings.key == 'AR-Time' %}
<input style="text-align:center" id="value" type="number" min="1" max="60" name="value" value="{{ settings.value }}">
{% elif settings.key == 'AR-MaxFeeRate' or settings.key == 'AF-MaxRate' %}
{% elif settings.key == 'AR-MaxFeeRate' %}
<input style="text-align:center" id="value" type="number" min="1" max="2500" name="value" value="{{ settings.value }}">
{% elif settings.key == 'AF-MaxRate' or settings.key == 'AF-MinRate' %}
<input style="text-align:center" id="value" type="number" min="0" max="5000" name="value" value="{{ settings.value }}">
@ -227,4 +227,4 @@
</table>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -28,7 +28,7 @@
<footer>
<div id="footer">
<div class="w3-container w3-padding-small">
<center>LNDg v1.2.1</center>
<center>LNDg v1.3.0</center>
</div>
</div>
</footer>

View file

@ -217,9 +217,9 @@
<td>{{ forward.amt_out|intcomma }}</td>
<td>{% if forward.chan_in_alias == '' %}---{% else %}{{ forward.chan_in_alias }}{% endif %}</td>
<td>{% if forward.chan_out_alias == '' %}---{% else %}{{ forward.chan_out_alias }}{% endif %}</td>
<td>{{ forward.chan_id_in }}</td>
<td>{{ forward.chan_id_out }}</td>
<td>{{ forward.fee }}</td>
<td><a href="/channel?={{ forward.chan_id_in }}" target="_blank">{{ forward.chan_id_in }}</a></td>
<td><a href="/channel?={{ forward.chan_id_out }}" target="_blank">{{ forward.chan_id_out }}</a></td>
<td>{{ forward.fee|intcomma }}</td>
<td>{{ forward.ppm|intcomma }}</td>
</tr>
{% endfor %}
@ -235,21 +235,25 @@
<th>Start</th>
<th>Stop</th>
<th>Scheduled Duration</th>
<th>Actual Duration</th>
<th>Value</th>
<th>Fee Limit</th>
<th>Target PPM</th>
<th>Fees Paid</th>
<th>Last Hop Alias</th>
<th>Status</th>
</tr>
{% for rebalance in rebalances %}
<tr>
<td title="{{ rebalance.requested }}">{{ rebalance.requested|naturaltime }}</td>
<td {% if rebalance.status == 0 %}>N/A{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %}</td>
<td {% if rebalance.status > 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>N/A{% endif %}</td>
<td {% if rebalance.status == 0 %}>---{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %}</td>
<td {% if rebalance.status > 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>---{% endif %}</td>
<td>{{ rebalance.duration }} minutes</td>
<td>{% if rebalance.status == 2 %}{{ rebalance.stop|timeuntil:rebalance.start }}{% else %}---{% endif %}</td>
<td>{{ rebalance.value|intcomma }}</td>
<td>{{ rebalance.fee_limit|intcomma }}</td>
<td>{{ rebalance.ppm|intcomma }}</td>
<td>{% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %}</td>
<td>{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}</td>
<td title="{{ rebalance.status }}">{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}<a href="/route?={{ rebalance.payment_hash }}" target="_blank">Successful</a>{% 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 %}</td>
</tr>
@ -281,9 +285,9 @@
<td>{{ payment.fee|intcomma }}</td>
<td>{{ payment.ppm|intcomma }}</td>
<td>{% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %}</td>
<td>{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}<a href="/route?={{ payment.payment_hash }}" target="_blank">Open</a>{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}---{% endif %}</td>
<td>{% if payment.status == 2 %}{{ payment.chan_out }}{% else %}---{% endif %}</td>
<td>{% if payment.status == 2 %}<a href="/route?={{ payment.payment_hash }}" target="_blank">Open</a>{% else %}---{% endif %}</td>
<td title="{{ payment.message }}">{% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %}</td>
</tr>
{% endfor %}
@ -308,13 +312,13 @@
{% for invoice in invoices %}
<tr>
<td title="{{ invoice.creation_date }}">{{ invoice.creation_date|naturaltime }}</td>
<td title="{{ invoice.settle_date }}">{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}N/A{% endif %}</td>
<td title="{{ invoice.settle_date }}">{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}---{% endif %}</td>
<td>{{ invoice.r_hash }}</td>
<td>{{ invoice.value|add:"0"|intcomma }}</td>
<td>{% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}---{% endif %}</td>
<td>{% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %}</td>
<td>{% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 and invoice.chan_in != None %}<a href="/channel?={{ invoice.chan_in }}" target="_blank">{{ invoice.chan_in }}</a>{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}---{% endif %}</td>
<td>{% if invoice.state == 1 and invoice.chan_in != None %}<a href="/channel?={{ invoice.chan_in }}" target="_blank">{{ invoice.chan_in }}</a>{% else %}---{% endif %}</td>
<td title="{{ invoice.message }}">{% if invoice.keysend_preimage != None %}Yes{% else %}No{% endif %}</td>
</tr>
{% endfor %}
@ -362,4 +366,4 @@
<center><h1>No data found for this channel!</h1></center>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -2,6 +2,105 @@
{% block title %} {{ block.super }} - Closures{% endblock %}
{% block content %}
{% load humanize %}
{% if pending_closed %}
<div class="w3-container w3-padding-small">
<h2>Pending Close Channels</h2>
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
<th>Channel ID</th>
<th>Peer Alias</th>
<th width=20%>Channel Point</th>
<th>Capacity</th>
<th>Local Balance</th>
<th>Remote Balance</th>
<th>Balance In Limbo</th>
<th>Local Commit Fees</th>
<th width=20%>Closing TX</th>
</tr>
{% for channel in pending_closed %}
<tr>
<td><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.remote_node_pub }}"><a href="{{ graph_links }}/{{ network }}node/{{ channel.remote_node_pub }}" target="_blank">{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}</a></td>
{% with funding_txid=channel.channel_point|slice:":-2" %}
<td><a href='{{ network_links }}/{{ network }}tx/{{ funding_txid }}' target="_blank">{{ channel.channel_point }}</a></td>
{% endwith %}
<td>{{ channel.capacity|intcomma }}</td>
<td>{{ channel.local_balance|intcomma }}</td>
<td>{{ channel.remote_balance|intcomma }}</td>
<td>{{ channel.limbo_balance|intcomma }}</td>
<td>{{ channel.local_commit_fee_sat }}</td>
<td><a href='{{ network_links }}/{{ network }}tx/{{ channel.closing_txid }}' target="_blank">{{ channel.closing_txid }}</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if pending_force_closed %}
<div class="w3-container w3-padding-small">
<h2>Pending Force Close Channels</h2>
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
<th>Channel ID</th>
<th>Peer Alias</th>
<th width=20%>Channel Point</th>
<th>Capacity</th>
<th>Local Balance</th>
<th>Remote Balance</th>
<th>Balance In Limbo</th>
<th>Maturity</th>
<th width=20%>Closing TX</th>
</tr>
{% for channel in pending_force_closed %}
<tr>
<td><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.remote_node_pub }}"><a href="{{ graph_links }}/{{ network }}node/{{ channel.remote_node_pub }}" target="_blank">{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}</a></td>
{% with funding_txid=channel.channel_point|slice:":-2" %}
<td><a href='{{ network_links }}/{{ network }}tx/{{ funding_txid }}' target="_blank">{{ channel.channel_point }}</a></td>
{% endwith %}
<td>{{ channel.capacity|intcomma }}</td>
<td>{{ channel.local_balance|intcomma }}</td>
<td>{{ channel.remote_balance|intcomma }}</td>
<td>{{ channel.limbo_balance|intcomma }}</td>
<td title="Blocks: {{ channel.blocks_til_maturity|intcomma }}">{{ channel.maturity_datetime|naturaltime }}</td>
<td><a href='{{ network_links }}/{{ network }}tx/{{ channel.closing_txid }}' target="_blank">{{ channel.closing_txid }}</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if waiting_for_close %}
<div class="w3-container w3-padding-small">
<h2>Channels Waiting To Close</h2>
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
<th>Channel ID</th>
<th>Peer Alias</th>
<th width=20%>Channel Point</th>
<th>Capacity</th>
<th>Local Balance</th>
<th>Remote Balance</th>
<th>Balance In Limbo</th>
<th>Local Commit Fee</th>
<th width=20%>Closing TX</th>
</tr>
{% for channel in waiting_for_close %}
<tr>
<td><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.remote_node_pub }}"><a href="{{ graph_links }}/{{ network }}node/{{ channel.remote_node_pub }}" target="_blank">{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}</a></td>
{% with funding_txid=channel.channel_point|slice:":-2" %}
<td><a href='{{ network_links }}/{{ network }}tx/{{ funding_txid }}' target="_blank">{{ channel.channel_point }}</a></td>
{% endwith %}
<td>{{ channel.capacity|intcomma }}</td>
<td>{{ channel.local_balance|intcomma }}</td>
<td>{{ channel.remote_balance|intcomma }}</td>
<td>{{ channel.limbo_balance|intcomma }}</td>
<td>{{ channel.local_commit_fee_sat }}</td>
<td><a href='{{ network_links }}/{{ network }}tx/{{ channel.closing_txid }}' target="_blank">{{ channel.closing_txid }}</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if closures %}
<div class="w3-container w3-padding-small">
<h2>Closures</h2>
@ -18,6 +117,7 @@
<th>Opener</th>
<th>Closer</th>
<th>Resolutions</th>
<th>Costs</th>
</tr>
{% for closure in closures %}
<tr>
@ -32,6 +132,20 @@
<td>{% 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 %}</td>
<td>{% 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 %}</td>
<td>{% if closure.resolution_count > 0 %}<a href="/resolutions?={{ closure.chan_id }}" target="_blank">Details</a>{% else %}---{% endif %}</td>
<td>
{% if closure.closing_costs == '' %}
---
{% elif closure.open_initiator == 2 and closure.close_type == 0 %}
---
{% else %}
<form action="/update_channel/" method="post">
{% csrf_token %}
<input style="text-align:center" id="target" type="number" min="0" max="1000000" name="target" value="{{ closure.closing_costs|add:"0" }}">
<input type="hidden" name="chan_id" value="{{ closure.chan_id }}">
<input type="hidden" name="update_target" value="10">
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
@ -42,4 +156,4 @@
<center><h1>No channel closures found!</h1></center>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -21,8 +21,8 @@
{% for failed_htlc in failed_htlcs %}
<tr>
<td title="{{ failed_htlc.timestamp }}">{{ failed_htlc.timestamp|naturaltime }}</td>
<td>{{ failed_htlc.chan_id_in }}</td>
<td>{{ failed_htlc.chan_id_out }}</td>
<td><a href="/channel?={{ failed_htlc.chan_id_in }}" target="_blank">{{ failed_htlc.chan_id_in }}</a></td>
<td><a href="/channel?={{ failed_htlc.chan_id_out }}" target="_blank">{{ failed_htlc.chan_id_out }}</a></td>
<td>{% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %}</td>
<td>{% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %}</td>
<td>{{ failed_htlc.amount|intcomma }}</td>
@ -40,4 +40,4 @@
<center><h1>You dont have any failed HTLCs yet.</h1></center>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -4,30 +4,28 @@
{% load humanize %}
<div class="w3-container w3-padding-small">
<h3><a href="{{ graph_links }}/{{ network }}node/{{ node_info.identity_pubkey }}" target="_blank">{{ node_info.alias }}</a> | {{ node_info.identity_pubkey }}</h3>
<h4>Public Capacity: {{ total_capacity|intcomma }} | Active Channels: {{ node_info.num_active_channels }} / {{ total_channels }} | <a href="/peers" target="_blank">Peers</a>: {{ node_info.num_peers }} | DB Size: {% if db_size > 0 %}{{ db_size }} GB{% else %}---{% endif %}</h4>
<h4>Public Capacity: {{ total_capacity|intcomma }} | Active Channels: {{ node_info.num_active_channels }} / {{ total_channels }} | <a href="/peers" target="_blank">Peers</a>: {{ 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 %}</h4>
{% if total_private > 0 %}<h4>Private Capacity: {{ private_capacity|intcomma }} | Locked Liquidity: {{ private_outbound|intcomma }} | Active Private Channels: {{ active_private }} / {{ total_private }}</h4>{% endif %}
<h4>Public Address: {% for info in node_info.uris %}{{ info }} | {% endfor %}</h4>
<h4>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 }}</h4>
</div>
<div class="w3-container w3-padding-small">
<h4>Wallet Balance: {{ balances.total_balance|intcomma }} | Confirmed Wallet Balance: {{ balances.confirmed_balance|intcomma }} | Unconfirmed Wallet Balance: {{ balances.unconfirmed_balance|intcomma }} | <a href="/balances" target="_blank">Details</a></h4>
<h4>Total Balance: {{ total_balance|intcomma }} | Onchain Balance: {{ balances.total_balance|intcomma }} | Confirmed Balance: {{ balances.confirmed_balance|intcomma }} | Unconfirmed Balance: {{ balances.unconfirmed_balance|intcomma }} | <a href="/balances" target="_blank">Details</a></h4>
<form action="/newaddress/" method="post">
{% csrf_token %}
<input type="submit" value="Get New Deposit Address">
<input type="submit" value="Get New Onchain Address">
</form>
</div>
<div class="w3-container w3-padding-small">
<h4>Completed Payments: {{ total_payments }} | Sats Sent: {{ total_sent|intcomma }} | Fees Paid: {{ fees_paid|intcomma }} [{{ payments_ppm|intcomma }}]</h4>
<h4>Paid Invoices: {{ total_invoices }} | Sats Received: {{ total_received|intcomma }}</h4>
<h4>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 }}%</h4>
<h4>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 }}%</h4>
<h4>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 }}%</h4>
<h4>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 }}%</h4>
</div>
<div class="w3-container w3-padding-small">
<h4>Total Inbound Liquidity: {{ sum_inbound|intcomma }} | Outbound Liquidity: {{ sum_outbound|intcomma }} | Liquidity Ratio: {{ liq_ratio }}%</h4>
<h4>Active Inbound Liquidity: {{ inbound|intcomma }} | Outbound Liquidity: {{ outbound|intcomma }} | Unsettled Liquidity: {{ unsettled|intcomma }}</h4>
<h4>Inactive Inbound Liquidity: {{ inactive_inbound|intcomma }} | Outbound Liquidity: {{ inactive_outbound|intcomma }} | Unsettled Liquidity: {{ inactive_unsettled|intcomma }}</h4>
<h4>Balance In Limbo: {{ limbo_balance|intcomma }} | Total Unsettled Liquidity: {{ total_unsettled|intcomma }} | <a href="/pending_htlcs" target="_blank">Pending HTLCs</a>: {{ pending_htlc_count }}</h4>
<h4><a href="/closures" target="_blank">Closures</a> | <a href="/opens" target="_blank">New Peers</a> | <a href="/actions" target="_blank">AR Actions</a> | <a href="/fees" target="_blank">Fee Rates</a> | <a href="/autopilot" target="_blank">Autopilot</a> | <a href="/autofees" target="_blank">Autofees</a> | <a href="/channels" target="_blank">Channel Performance</a> | <a href="/keysends" target="_blank">Keysends</a> | <a href="/rebalancing" target="_blank">Rebalancing</a> | <a href="/towers" target="_blank">Towers</a> | <a href="/batch" target="_blank">Batching</a> | <a href="/advanced" target="_blank">Advanced Settings</a></h4>
<h4><a href="/income" target="_blank">P&L</a> | <a href="/closures" target="_blank">Closures</a> | <a href="/opens" target="_blank">New Peers</a> | <a href="/actions" target="_blank">AR Actions</a> | <a href="/fees" target="_blank">Fee Rates</a> | <a href="/autopilot" target="_blank">Autopilot</a> | <a href="/autofees" target="_blank">Autofees</a> | <a href="/channels" target="_blank">Channel Performance</a> | <a href="/keysends" target="_blank">Keysends</a> | <a href="/rebalancing" target="_blank">Rebalancing</a> | <a href="/towers" target="_blank">Towers</a> | <a href="/batch" target="_blank">Batching</a> | <a href="/advanced" target="_blank">Advanced Settings</a></h4>
</div>
{% if active_channels %}
<div class="w3-container w3-padding-small">
@ -44,10 +42,10 @@
<th>Unsettled</th>
<th>oRate</th>
<th>oBase</th>
<th>o1D</th>
<th>i1D</th>
<th>o7D</th>
<th>i7D</th>
<th>oLife</th>
<th>iLife</th>
<th>iRate</th>
<th>iBase</th>
<th title="When AR is ENABLED for the channel, keep pulling IN to the channel until its inbound liquidity falls below the iTarget%." width=4%>iTarget%</th>
@ -64,10 +62,10 @@
<td>{{ channel.unsettled_balance|intcomma }} ({{ channel.htlc_count }})</td>
<td {% if channel.local_disabled == True %}style="background-color: #fadbd5"{% endif %}>{{ channel.local_fee_rate|intcomma }}</td>
<td {% if channel.local_disabled == True %}style="background-color: #fadbd5"{% endif %}>{{ channel.local_base_fee|intcomma }}</td>
<td>{{ channel.amt_routed_out_1day|intcomma }} M ({{ channel.routed_out_1day }})</td>
<td>{{ channel.amt_routed_in_1day|intcomma }} M ({{ channel.routed_in_1day }})</td>
<td>{{ channel.amt_routed_out_7day|intcomma }} M ({{ channel.routed_out_7day }})</td>
<td>{{ channel.amt_routed_in_7day|intcomma }} M ({{ channel.routed_in_7day }})</td>
<td>{{ channel.amt_routed_out|intcomma }} M ({{ channel.routed_out }})</td>
<td>{{ channel.amt_routed_in|intcomma }} M ({{ channel.routed_in }})</td>
<td {% if channel.remote_disabled == True %}style="background-color: #fadbd5"{% endif %}>{{ channel.remote_fee_rate|intcomma }}</td>
<td {% if channel.remote_disabled == True %}style="background-color: #fadbd5"{% endif %}>{{ channel.remote_base_fee|intcomma }}</td>
<td>
@ -216,18 +214,106 @@
<th>Capacity</th>
<th>Local Balance</th>
<th>Remote Balance</th>
<th>Commit Fee</th>
<th>AF</th>
<th>Fee Rate</th>
<th>Base Fee</th>
<th>CLTV</th>
<th>Amt</th>
<th>Max Cost%</th>
<th>oTarget%</th>
<th>iTarget%</th>
<th>AR</th>
</tr>
{% for channel in pending_open %}
<tr>
<td title="{{ channel.remote_node_pub }}"><a href="{{ graph_links }}/{{ network }}node/{{ channel.remote_node_pub }}" target="_blank">{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}</a></td>
{% with funding_txid=channel.channel_point|slice:":-2" %}
<td><a href='{{ network_links }}/{{ network }}tx/{{ funding_txid }}' target="_blank">{{ channel.channel_point }}</a></td>
{% endwith %}
<td>{{ channel.capacity|intcomma }}</td>
<td><a href='{{ network_links }}/{{ network }}tx/{{ channel.funding_txid }}' target="_blank">{{ channel.channel_point }}</a></td>
<td title="Commit Fee: {{ channel.commit_fee }}">{{ channel.capacity|intcomma }}</td>
<td>{{ channel.local_balance|intcomma }}</td>
<td>{{ channel.remote_balance|intcomma }}</td>
<td>{{ channel.commit_fee }}</td>
<td {% if channel.auto_fees == False %}style="background-color: #fadbd5"{% else %}style="background-color: #a6dce2"{% endif %}>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input type="submit" value="{% if channel.auto_fees == True %}Disable{% else %}Enable{% endif %}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="8">
<input type="hidden" name="target" value="0">
</form>
</td>
<td>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input style="text-align:center" id="target" type="number" min="0" max="100000" name="target" value="{{ channel.local_fee_rate }}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="1">
</form>
</td>
<td>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input style="text-align:center" id="target" type="number" min="0" max="100000" name="target" value="{{ channel.local_base_fee }}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="0">
</form>
</td>
<td>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input style="text-align:center" id="target" type="number" min="18" max="1000" name="target" value="{{ channel.local_cltv }}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="9">
</form>
</td>
<td>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input style="text-align:center" id="target" type="number" min="1" max="100000000" name="target" value="{{ channel.ar_amt_target }}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="2">
</form>
</td>
<td>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input style="text-align:center" id="target" type="number" min="1" max="100" name="target" value="{{ channel.ar_max_cost }}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="6">
</form>
</td>
<td {% if channel.auto_rebalance == False %}style="background-color: #80ced6"{% else %}style="background-color: #f18973"{% endif %}>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input style="text-align:center" id="target" type="number" min="1" max="100" name="target" value="{{ channel.ar_out_target }}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="4">
</form>
</td>
<td {% if channel.auto_rebalance == True %}style="background-color: #80ced6"{% else %}style="background-color: #f18973"{% endif %}>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input style="text-align:center" id="target" type="number" min="1" max="100" name="target" value="{{ channel.ar_in_target }}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="3">
</form>
</td>
<td>
<form action="/update_pending/" method="post">
{% csrf_token %}
<input type="submit" value="{% if channel.auto_rebalance == True %}Disable{% else %}Enable{% endif %}">
<input type="hidden" name="funding_txid" value="{{ channel.funding_txid }}">
<input type="hidden" name="output_index" value="{{ channel.output_index }}">
<input type="hidden" name="update_target" value="5">
<input type="hidden" name="target" value="0">
</form>
</td>
</tr>
{% endfor %}
</table>
@ -240,25 +326,23 @@
<tr>
<th>Channel ID</th>
<th>Peer Alias</th>
<th width=20%>Channel Point</th>
<th>Capacity</th>
<th>Local Balance</th>
<th>Remote Balance</th>
<th>Balance In Limbo</th>
<th>Local Commit Fee</th>
<th width=20%>Closing TX</th>
</tr>
{% for channel in pending_closed %}
<tr>
<td><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.channel_point }}"><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.remote_node_pub }}"><a href="{{ graph_links }}/{{ network }}node/{{ channel.remote_node_pub }}" target="_blank">{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}</a></td>
{% with funding_txid=channel.channel_point|slice:":-2" %}
<td><a href='{{ network_links }}/{{ network }}tx/{{ funding_txid }}' target="_blank">{{ channel.channel_point }}</a></td>
{% endwith %}
<td>{{ channel.capacity|intcomma }}</td>
<td>{{ channel.local_balance|intcomma }}</td>
<td>{{ channel.remote_balance|intcomma }}</td>
<td>{{ channel.limbo_balance|intcomma }}</td>
<td>{{ channel.commitments.local_commit_fee_sat }}</td>
<td>{{ channel.local_commit_fee_sat|intcomma }}</td>
<td><a href='{{ network_links }}/{{ network }}tx/{{ channel.closing_txid }}' target="_blank">{{ channel.closing_txid }}</a></td>
</tr>
{% endfor %}
</table>
@ -271,7 +355,6 @@
<tr>
<th>Channel ID</th>
<th>Peer Alias</th>
<th width=20%>Channel Point</th>
<th>Capacity</th>
<th>Local Balance</th>
<th>Remote Balance</th>
@ -281,11 +364,8 @@
</tr>
{% for channel in pending_force_closed %}
<tr>
<td><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.channel_point }}"><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.remote_node_pub }}"><a href="{{ graph_links }}/{{ network }}node/{{ channel.remote_node_pub }}" target="_blank">{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}</a></td>
{% with funding_txid=channel.channel_point|slice:":-2" %}
<td><a href='{{ network_links }}/{{ network }}tx/{{ funding_txid }}' target="_blank">{{ channel.channel_point }}</a></td>
{% endwith %}
<td>{{ channel.capacity|intcomma }}</td>
<td>{{ channel.local_balance|intcomma }}</td>
<td>{{ channel.remote_balance|intcomma }}</td>
@ -304,25 +384,23 @@
<tr>
<th>Channel ID</th>
<th>Peer Alias</th>
<th width=20%>Channel Point</th>
<th>Capacity</th>
<th>Local Balance</th>
<th>Remote Balance</th>
<th>Balance In Limbo</th>
<th>Local Commit Fee</th>
<th width=20%>Closing TX</th>
</tr>
{% for channel in waiting_for_close %}
<tr>
<td><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.channel_point }}"><a href="/channel?={{ channel.chan_id }}" target="_blank">{{ channel.chan_id }}</a></td>
<td title="{{ channel.remote_node_pub }}"><a href="{{ graph_links }}/{{ network }}node/{{ channel.remote_node_pub }}" target="_blank">{% if channel.alias == '' %}{{ channel.remote_node_pub|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}</a></td>
{% with funding_txid=channel.channel_point|slice:":-2" %}
<td><a href='{{ network_links }}/{{ network }}tx/{{ funding_txid }}' target="_blank">{{ channel.channel_point }}</a></td>
{% endwith %}
<td>{{ channel.capacity|intcomma }}</td>
<td>{{ channel.local_balance|intcomma }}</td>
<td>{{ channel.remote_balance|intcomma }}</td>
<td>{{ channel.limbo_balance|intcomma }}</td>
<td>{{ channel.commitments.local_commit_fee_sat }}</td>
<td>{{ channel.local_commit_fee_sat|intcomma }}</td>
<td><a href='{{ network_links }}/{{ network }}tx/{{ channel.closing_txid }}' target="_blank">{{ channel.closing_txid }}</a></td>
</tr>
{% endfor %}
</table>
@ -352,7 +430,7 @@
<td>{% if forward.chan_out_alias == '' %}---{% else %}{{ forward.chan_out_alias }}{% endif %}</td>
<td><a href="/channel?={{ forward.chan_id_in }}" target="_blank">{{ forward.chan_id_in }}</a></td>
<td><a href="/channel?={{ forward.chan_id_out }}" target="_blank">{{ forward.chan_id_out }}</a></td>
<td>{{ forward.fee }}</td>
<td>{{ forward.fee|intcomma }}</td>
<td>{{ forward.ppm|intcomma }}</td>
</tr>
{% endfor %}
@ -361,28 +439,32 @@
{% endif %}
{% if rebalances %}
<div class="w3-container w3-padding-small">
<h2>Last 10 <a href="/rebalancing" target="_blank">Rebalance Requests</a> (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing)</h2>
<h2>Last 10 <a href="/rebalances" target="_blank">Rebalance Requests</a> (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing via {{ available_count }} outbound channels)</h2>
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
<th>Requested</th>
<th>Start</th>
<th>Stop</th>
<th>Scheduled Duration</th>
<th>Actual Duration</th>
<th>Value</th>
<th>Fee Limit</th>
<th>Target PPM</th>
<th>Fees Paid</th>
<th>Last Hop Alias</th>
<th>Status</th>
</tr>
{% for rebalance in rebalances %}
<tr>
<td title="{{ rebalance.requested }}">{{ rebalance.requested|naturaltime }}</td>
<td title="{{ rebalance.start }}">{% if rebalance.status == 0 %}N/A{% else %}{{ rebalance.start|naturaltime }}{% endif %}</td>
<td title="{{ rebalance.stop }}">{% if rebalance.status > 1 %}{{ rebalance.stop|naturaltime }}{% else %}N/A{% endif %}</td>
<td title="{{ rebalance.start }}">{% if rebalance.status == 0 %}---{% else %}{{ rebalance.start|naturaltime }}{% endif %}</td>
<td title="{{ rebalance.stop }}">{% if rebalance.status > 1 %}{{ rebalance.stop|naturaltime }}{% else %}---{% endif %}</td>
<td>{{ rebalance.duration }} minutes</td>
<td>{% if rebalance.status == 2 %}{{ rebalance.stop|timeuntil:rebalance.start }}{% else %}---{% endif %}</td>
<td>{{ rebalance.value|intcomma }}</td>
<td>{{ rebalance.fee_limit|intcomma }}</td>
<td>{{ rebalance.ppm|intcomma }}</td>
<td>{% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma}}{% else %}---{% endif %}</td>
<td>{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}</td>
<td title="{{ rebalance.status }}">{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}<a href="/route?={{ rebalance.payment_hash }}" target="_blank">Successful</a>{% 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 %}</td>
</tr>
@ -414,9 +496,9 @@
<td>{{ payment.fee|intcomma }}</td>
<td>{{ payment.ppm|intcomma }}</td>
<td>{% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %}</td>
<td>{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}{% if payment.chan_out != 'MPP' %}<a href="/channel?={{ payment.chan_out }}" target="_blank">{{ payment.chan_out }}</a>{% else %}{{ payment.chan_out }}{% endif %}{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}<a href="/route?={{ payment.payment_hash }}" target="_blank">Open</a>{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}---{% endif %}</td>
<td>{% if payment.status == 2 %}{% if payment.chan_out != 'MPP' %}<a href="/channel?={{ payment.chan_out }}" target="_blank">{{ payment.chan_out }}</a>{% else %}{{ payment.chan_out }}{% endif %}{% else %}---{% endif %}</td>
<td>{% if payment.status == 2 %}<a href="/route?={{ payment.payment_hash }}" target="_blank">Open</a>{% else %}---{% endif %}</td>
<td title="{{ payment.message }}">{% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %}</td>
</tr>
{% endfor %}
@ -441,13 +523,13 @@
{% for invoice in invoices %}
<tr>
<td title="{{ invoice.creation_date }}">{{ invoice.creation_date|naturaltime }}</td>
<td title="{{ invoice.settle_date }}">{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}N/A{% endif %}</td>
<td title="{{ invoice.settle_date }}">{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}---{% endif %}</td>
<td>{{ invoice.r_hash }}</td>
<td>{{ invoice.value|add:"0"|intcomma }}</td>
<td>{% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}---{% endif %}</td>
<td>{% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %}</td>
<td>{% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 and invoice.chan_in != None %}<a href="/channel?={{ invoice.chan_in }}" target="_blank">{{ invoice.chan_in }}</a>{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}---{% endif %}</td>
<td>{% if invoice.state == 1 and invoice.chan_in != None %}<a href="/channel?={{ invoice.chan_in }}" target="_blank">{{ invoice.chan_in }}</a>{% else %}---{% endif %}</td>
<td title="{{ invoice.message }}">{% if invoice.keysend_preimage != None %}Yes{% else %}No{% endif %}</td>
</tr>
{% endfor %}
@ -517,6 +599,8 @@
<input id="target_time" type="number" min="1" max="60" name="target_time">
<label title="When a channel is not enabled for targeting; the minimum outbound a channel must have to be a source for refilling another channel" for="outbound_percent">Target Outbound Above (%): </label>
<input id="outbound_percent" type="number" step="1" min="1" max="100" name="outbound_percent">
<label title="When a channel is enabled for targeting; the maximum inbound a channel can have before selected for auto rebalance" for="inbound_percent">Target Inbound Above (%): </label>
<input id="inbound_percent" type="number" step="1" min="1" max="100" name="inbound_percent">
<label title="The max rate we can ever use to refill a channel with outbound" for="fee_rate">Global Max Fee Rate (ppm): </label>
<input id="fee_rate" type="number" min="1" max="2500" name="fee_rate">
<label title="The ppm to target which is the % of the outbound fee rate for the channel being refilled" for="max_cost">Max Cost (%): </label>
@ -527,6 +611,10 @@
<input id="wait_period" type="number" min="1" max="100" name="wait_period">
<label title="This enables or disables the Autopilot function which automatically acts upon suggestions on this page: /actions" for="autopilot">Autopilot: </label>
<input id="autopilot" type="number" min="0" max="1" name="autopilot">
<label title="Number of days to consider for autopilot. Default 7." for="autopilotdays">AutopilotDays: </label>
<input id="autopilotdays" type="number" min="0" max="100" name="autopilotdays">
<label title="Apply for all existing channels." for="targetallchannels">Target All Channels:? </label>
<input id="targetallchannels" type="checkbox" name="targetallchannels">
<input type="submit" value="OK">
</form>
</div>

74
gui/templates/income.html Normal file
View file

@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %} {{ block.super }} - P&L{% endblock %}
{% block content %}
{% load humanize %}
<div class="w3-container w3-padding-small">
<h2>P&L Statement For <a href="{{ graph_links }}/{{ network }}node/{{ node_info.identity_pubkey }}" target="_blank">{% if node_info.alias != "" %}{{ node_info.alias }}</a> ({{ node_info.identity_pubkey }}){% else %}{{ node_info.identity_pubkey }}</a>{% endif %}</h2>
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
<th>Line Item</th>
<th>1 Day</th>
<th>7 Day</th>
<th>30 Day</th>
<th>90 Day</th>
<th>Lifetime</th>
</tr>
<tr>
<td>Payments Routed</td>
<td>{{ forward_count_1day|intcomma }}</td>
<td>{{ forward_count_7day|intcomma }}</td>
<td>{{ forward_count_30day|intcomma }}</td>
<td>{{ forward_count_90day|intcomma }}</td>
<td>{{ forward_count|intcomma }}</td>
</tr>
<tr>
<td>Value Routed</td>
<td>{{ forward_amount_1day|intcomma }}</td>
<td>{{ forward_amount_7day|intcomma }}</td>
<td>{{ forward_amount_30day|intcomma }}</td>
<td>{{ forward_amount_90day|intcomma }}</td>
<td>{{ forward_amount|intcomma }}</td>
</tr>
<tr>
<td>Revenue Earned</td>
<td>{{ total_revenue_1day|intcomma }} [{{ total_revenue_ppm_1day|intcomma }}]</td>
<td>{{ total_revenue_7day|intcomma }} [{{ total_revenue_ppm_7day|intcomma }}]</td>
<td>{{ total_revenue_30day|intcomma }} [{{ total_revenue_ppm_30day|intcomma }}]</td>
<td>{{ total_revenue_90day|intcomma }} [{{ total_revenue_ppm_90day|intcomma }}]</td>
<td>{{ total_revenue|intcomma }} [{{ total_revenue_ppm|intcomma }}]</td>
</tr>
<tr>
<td>Onchain Costs</td>
<td>{{ onchain_costs_1day|intcomma }}</td>
<td>{{ onchain_costs_7day|intcomma }}</td>
<td>{{ onchain_costs_30day|intcomma }}</td>
<td>{{ onchain_costs_90day|intcomma }}</td>
<td>{{ onchain_costs|intcomma }}</td>
</tr>
<tr>
<td>Offchain Costs</td>
<td>{{ total_fees_1day|intcomma }} [{{ total_fees_ppm_1day }}]</td>
<td>{{ total_fees_7day|intcomma }} [{{ total_fees_ppm_7day }}]</td>
<td>{{ total_fees_30day|intcomma }} [{{ total_fees_ppm_30day }}]</td>
<td>{{ total_fees_90day|intcomma }} [{{ total_fees_ppm_90day }}]</td>
<td>{{ total_fees|intcomma }} [{{ total_fees_ppm }}]</td>
</tr>
<tr>
<td>Percent Cost</td>
<td>{{ percent_cost_1day|intcomma }}%</td>
<td>{{ percent_cost_7day|intcomma }}%</td>
<td>{{ percent_cost_30day|intcomma }}%</td>
<td>{{ percent_cost_90day|intcomma }}%</td>
<td>{{ percent_cost|intcomma }}%</td>
</tr>
<tr>
<td>Profits</td>
<td>{{ profits_1day|intcomma }} [{{ profits_ppm_1day }}]</td>
<td>{{ profits_7day|intcomma }} [{{ profits_ppm_7day }}]</td>
<td>{{ profits_30day|intcomma }} [{{ profits_ppm_30day }}]</td>
<td>{{ profits_90day|intcomma }} [{{ profits_ppm_90day }}]</td>
<td>{{ profits|intcomma }} [{{ profits_ppm }}]</td>
</tr>
</table>
</div>
{% endblock %}

View file

@ -20,13 +20,13 @@
{% for invoice in invoices %}
<tr>
<td title="{{ invoice.creation_date }}">{{ invoice.creation_date|naturaltime }}</td>
<td title="{{ invoice.settle_date }}">{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}N/A{% endif %}</td>
<td title="{{ invoice.settle_date }}">{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}---{% endif %}</td>
<td>{{ invoice.r_hash }}</td>
<td>{{ invoice.value|add:"0"|intcomma }}</td>
<td>{% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}---{% endif %}</td>
<td>{% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %}</td>
<td>{% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 and invoice.chan_in != None %}<a href="/channel?={{ invoice.chan_in }}" target="_blank">{{ invoice.chan_in }}</a>{% else %}N/A{% endif %}</td>
<td>{% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}---{% endif %}</td>
<td>{% if invoice.state == 1 and invoice.chan_in != None %}<a href="/channel?={{ invoice.chan_in }}" target="_blank">{{ invoice.chan_in }}</a>{% else %}---{% endif %}</td>
<td title="{{ invoice.message }}">{% if invoice.keysend_preimage != None %}Yes{% else %}No{% endif %}</td>
</tr>
{% endfor %}
@ -38,4 +38,4 @@
<center><h1>You dont have any invoices yet.</h1></center>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -21,7 +21,7 @@
<tr>
<td><a href="{{ graph_links }}/{{ network }}node/{{ node.node_pubkey }}" target="_blank">{{ node.node_pubkey }}</a></td>
<td>{% if node.alias == '' %}---{% else %}{{ node.alias }}{% endif %}</td>
<td>{{ node.count }}</td>
<td><a href="/routes?={{ node.node_pubkey }}" target="_blank">{{ node.count }}</a></td>
<td>{{ node.amount|add:"0"|intcomma }}</td>
<td>{{ node.fees|add:"0"|intcomma }}</td>
<td>{{ node.ppm|add:"0"|intcomma }}</td>
@ -38,4 +38,4 @@
<center><h1>No potential peers can be calculated yet, try waiting until you have some payment data.</h1></center>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -26,9 +26,9 @@
<td>{{ payment.fee|intcomma }}</td>
<td>{{ payment.ppm|intcomma }}</td>
<td>{% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %}</td>
<td>{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}<a href="/channel?={{ payment.chan_out }}" target="_blank">{{ payment.chan_out }}</a>{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}<a href="/route?={{ payment.payment_hash }}" target="_blank">Open</a>{% else %}N/A{% endif %}</td>
<td>{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}---{% endif %}</td>
<td>{% if payment.status == 2 %}<a href="/channel?={{ payment.chan_out }}" target="_blank">{{ payment.chan_out }}</a>{% else %}---{% endif %}</td>
<td>{% if payment.status == 2 %}<a href="/route?={{ payment.payment_hash }}" target="_blank">Open</a>{% else %}---{% endif %}</td>
<td title="{{ payment.message }}">{% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %}</td>
</tr>
{% endfor %}
@ -40,4 +40,4 @@
<center><h1>You dont have any payments yet.</h1></center>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -17,9 +17,9 @@
</tr>
{% for htlc in outgoing_htlcs %}
<tr>
<td>{{ htlc.chan_id }}</td>
<td><a href="/channel?={{ htlc.chan_id }}" target="_blank">{{ htlc.chan_id }}</a></td>
<td>{% if htlc.alias == '' %}---{% else %}{{ htlc.alias }}{% endif %}</td>
<td>{{ htlc.forwarding_channel }}</td>
<td>{% if htlc.forwarding_channel == 0 %}---{% else %}<a href="/channel?={{ htlc.forwarding_channel }}" target="_blank">{{ htlc.forwarding_channel }}</a>{% endif %}</td>
<td>{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %}</td>
<td>{{ htlc.amount|intcomma }}</td>
<td title="{{ htlc.blocks_til_expiration|intcomma }} blocks to {{ htlc.expiration_height|intcomma }}">{{ htlc.hours_til_expiration }} hours</td>
@ -44,9 +44,9 @@
</tr>
{% for htlc in incoming_htlcs %}
<tr>
<td>{{ htlc.chan_id }}</td>
<td><a href="/channel?={{ htlc.chan_id }}" target="_blank">{{ htlc.chan_id }}</a></td>
<td>{% if htlc.alias == '' %}---{% else %}{{ htlc.alias }}{% endif %}</td>
<td>{{ htlc.forwarding_channel }}</td>
<td>{% if htlc.forwarding_channel == 0 %}---{% else %}<a href="/channel?={{ htlc.forwarding_channel }}" target="_blank">{{ htlc.forwarding_channel }}</a>{% endif %}</td>
<td>{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %}</td>
<td>{{ htlc.amount|intcomma }}</td>
<td title="{{ htlc.blocks_til_expiration|intcomma }} blocks to {{ htlc.expiration_height|intcomma }}">{{ htlc.hours_til_expiration }} hours</td>
@ -61,4 +61,4 @@
<center><h1>No pending HTLCs were found!</h1></center>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %} {{ block.super }} - Rebalances{% endblock %}
{% block content %}
{% load humanize %}
{% if rebalances %}
<div class="w3-container w3-padding-small">
<h2>Last 150 Rebalances</h2>
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
<th>Requested</th>
<th>Start</th>
<th>Stop</th>
<th>Scheduled Duration</th>
<th>Actual Duration</th>
<th>Value</th>
<th>Fee Limit</th>
<th>Target PPM</th>
<th>Fees Paid</th>
<th>Last Hop Alias</th>
<th>Status</th>
</tr>
{% for rebalance in rebalances %}
<tr>
<td title="{{ rebalance.requested }}">{{ rebalance.requested|naturaltime }}</td>
<td {% if rebalance.status == 0 %}>---{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %}</td>
<td {% if rebalance.status > 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>---{% endif %}</td>
<td>{{ rebalance.duration }} minutes</td>
<td>{% if rebalance.status == 2 %}{{ rebalance.stop|timeuntil:rebalance.start }}{% else %}---{% endif %}</td>
<td>{{ rebalance.value|intcomma }}</td>
<td>{{ rebalance.fee_limit|intcomma }}</td>
<td>{{ rebalance.ppm|intcomma }}</td>
<td>{% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %}</td>
<td>{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}</td>
<td title="{{ rebalance.status }}">{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}<a href="/route?={{ rebalance.payment_hash }}" target="_blank">Successful</a>{% 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 %}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if not rebalances %}
<div class="w3-container w3-padding-small">
<center><h1>You dont have any rebalances yet.</h1></center>
</div>
{% endif %}
{% endblock %}

View file

@ -4,7 +4,7 @@
{% load humanize %}
{% if channels %}
<div class="w3-container w3-padding-small">
<h2>Channel Rebalancing (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing)</h2>
<h2>Channel Rebalancing (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing via {{ available_count }} outbound channels)</h2>
<div class="w3-container w3-padding-small" style="overflow-x: scroll">
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
@ -21,7 +21,7 @@
<th title="When AR is ENABLED for the channel, the size of the rebalance attempts that should be tried during attempts to refill the channel.">Target Amt</th>
<th title="When AR is ENABLED, the maximum percentage amount of the local fee rate that can be used for the max rebalancing cost.">Max Cost %</th>
<th title="When AR is NOT ENABLED for the channel, keep pushing OUT the channel until its outbound liquidity falls below the oTarget%.">oTarget%</th>
<th>iTarget%</th>
<th title="When AR is ENABLED for the channel, keep pulling IN the channel until its inbound liquidity is above the iTarget%.">iTarget%</th>
<th>AR</th>
<th title="The rate of successful rebalances on this channel.">7-Day Rate</th>
<th>Active</th>
@ -94,28 +94,32 @@
{% endif %}
{% if rebalancer %}
<div class="w3-container w3-padding-small">
<h2>Last 20 Rebalance Requests</h2>
<h2>Last 20 <a href="/rebalances" target="_blank">Rebalance Requests</a></h2>
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
<th>Requested</th>
<th>Start</th>
<th>Stop</th>
<th>Scheduled Duration</th>
<th>Actual Duration</th>
<th>Value</th>
<th>Fee Limit</th>
<th>Target PPM</th>
<th>Fees Paid</th>
<th>Last Hop Alias</th>
<th>Status</th>
</tr>
{% for rebalance in rebalancer %}
<tr>
<td title="{{ rebalance.requested }}">{{ rebalance.requested|naturaltime }}</td>
<td {% if rebalance.status == 0 %}>N/A{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %}</td>
<td {% if rebalance.status > 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>N/A{% endif %}</td>
<td {% if rebalance.status == 0 %}>---{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %}</td>
<td {% if rebalance.status > 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>---{% endif %}</td>
<td>{{ rebalance.duration }} minutes</td>
<td>{% if rebalance.status == 2 %}{{ rebalance.stop|timeuntil:rebalance.start }}{% else %}---{% endif %}</td>
<td>{{ rebalance.value|intcomma }}</td>
<td>{{ rebalance.fee_limit|intcomma }}</td>
<td>{{ rebalance.ppm|intcomma }}</td>
<td>{% if rebalance.status == 2 %}{{ rebalance.fees_paid|intcomma }}{% else %}---{% endif %}</td>
<td>{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}</td>
<td title="{{ rebalance.status }}">{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}<a href="/route?={{ rebalance.payment_hash }}" target="_blank">Successful</a>{% 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 %}</td>
</tr>
@ -158,6 +162,8 @@
<input id="target_time" type="number" min="1" max="60" name="target_time">
<label title="When a channel is not enabled for targeting; the minimum outbound a channel must have to be a source for refilling another channel" for="outbound_percent">Target Outbound Above (%): </label>
<input id="outbound_percent" type="number" step="1" min="1" max="100" name="outbound_percent">
<label title="When a channel is enabled for targeting; the maximum inbound a channel can have before selected for auto rebalance" for="inbound_percent">Target Inbound Above (%): </label>
<input id="inbound_percent" type="number" step="1" min="1" max="100" name="inbound_percent">
<label title="The max rate we can ever use to refill a channel with outbound" for="fee_rate">Global Max Fee Rate (ppm): </label>
<input id="fee_rate" type="number" min="1" max="2500" name="fee_rate">
<label title="The ppm to target which is the % of the outbound fee rate for the channel being refilled" for="max_cost">Max Cost (%): </label>
@ -168,6 +174,10 @@
<input id="wait_period" type="number" min="1" max="100" name="wait_period">
<label title="This enables or disables the Autopilot function which automatically acts upon suggestions on this page: /actions" for="autopilot">Autopilot: </label>
<input id="autopilot" type="number" min="0" max="1" name="autopilot">
<label title="Number of days to consider for autopilot. Default 7. " for="autopilotdays">AutopilotDays: </label>
<input id="autopilotdays" type="number" min="0" max="100" name="autopilotdays">
<label title="Target All Channels for applicable channel level settings. Uncheck if you only want to update local settings for future channels." for="targetallchannels">AllChannels?: </label>
<input id="targetallchannels" type="checkbox" name="targetallchannels">
<input type="submit" value="OK">
</form>
</div>
@ -197,4 +207,4 @@
</div>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -19,7 +19,7 @@
<td>{% 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 %}</td>
<td><a href="{{ network_links }}/{{ network }}tx/{{ resolution.outpoint_tx }}" target="_blank">{{ resolution.outpoint_tx }}:{{ resolution.outpoint_index }}</a></td>
<td>{{ resolution.amount_sat|intcomma }}</td>
<td><a href="{{ network_links }}/{{ network }}tx/{{ resolution.sweep_txid }}" target="_blank">{{ resolution.sweep_txid }}</a></td>
<td>{% if resolution.resolution_type != 2 %}<a href="{{ network_links }}/{{ network }}tx/{{ resolution.sweep_txid }}" target="_blank">{{ resolution.sweep_txid }}</a>{% else %}---{% endif %}</td>
</tr>
{% endfor %}
</table>

View file

@ -4,7 +4,7 @@
{% load humanize %}
{% if route %}
<div class="w3-container w3-padding-small">
<h2>Route For Payment: {{ payment_hash }}</h2>
<h2>Route For : {{ payment_hash }}</h2>
<table class="w3-table-all w3-centered w3-hoverable">
<tr>
<th>Step</th>
@ -12,6 +12,7 @@
<th>Fee</th>
<th title="The fee in PPM paid on this hop.">PPM</th>
<th title="The cost to get the payment to this hop of the payment.">Cost To</th>
<th>📍</th>
<th>Alias</th>
<th>Channel ID</th>
<th>Channel Capacity</th>
@ -23,8 +24,9 @@
<td>{{ hop.fee|intcomma }}</td>
<td>{{ hop.ppm|intcomma }}</td>
<td>{{ hop.cost_to|intcomma }}</td>
<td>{% if hop.step == 1 %}📍{% else %}🔻{% endif %}
<td>{% if hop.alias == '' %}---{% else %}{{ hop.alias }}{% endif %}</td>
<td>{{ hop.chan_id }}</td>
<td><a href="/channel?={{ hop.chan_id }}" target="_blank">{{ hop.chan_id }}</a></td>
<td>{{ hop.chan_capacity|intcomma }}</td>
</tr>
{% endfor %}
@ -36,4 +38,4 @@
<center><h1>A route was not found for this payment hash!</h1></center>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -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),
]
]

View file

@ -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})
return Response({'error': 'Failed to get pending channels! Error: ' + error_msg})

View file

@ -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()

255
jobs.py
View file

@ -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():

View file

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

View file

@ -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()
main()

View file

@ -4,4 +4,5 @@ django-qr-code
grpcio
protobuf
pytz
pandas
pandas
requests