From 3ee02496e4ac4b78fe5793814fa7c70a5b020e34 Mon Sep 17 00:00:00 2001 From: cryptosharks131 <38626122+cryptosharks131@users.noreply.github.com> Date: Thu, 30 Dec 2021 17:24:20 -0500 Subject: [PATCH] v1.0.0 (#14) * Suggested AR Actions * Connect to peer with only pubkey or pubkey@address * Open channel without connecting first * Login required for dashboard access --- .gitignore | 3 +- README.md | 56 ++-- docker-compose.yaml | 4 +- gui/forms.py | 3 +- gui/serializers.py | 3 +- gui/templates/action_list.html | 66 ++++ gui/templates/base.html | 7 + gui/templates/home.html | 38 +-- gui/templates/testnet.html | 585 --------------------------------- gui/urls.py | 4 + gui/views.py | 229 ++++++++++--- initialize.py | 75 ++++- 12 files changed, 391 insertions(+), 682 deletions(-) create mode 100644 gui/templates/action_list.html delete mode 100644 gui/templates/testnet.html diff --git a/.gitignore b/.gitignore index 348131c1..d1abb459 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__ db.sqlite3 lndg/settings.py frontend -node_modules \ No newline at end of file +node_modules +lndg-admin.txt \ No newline at end of file diff --git a/README.md b/README.md index d3eca066..e53cedbd 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,11 @@ Start by choosing one of the following installation methods: ### Build and deploy 1. Clone respository `git clone https://github.com/cryptosharks131/lndg.git` 2. Change directory into the repo `cd lndg` -3. Customize `docker-compose.yaml` if you like and then build/deploy your docker image: `docker-compose up -d` -4. LNDg should now be available on port `8889` +3. Initialize db and admin backup `touch db.sqlite3 && touch lndg-admin.txt` +4. Update `docker-compose.yaml` if you are a non-root user and then build/deploy your docker image: `docker-compose up -d` +5. LNDg should now be available on port `8889` +6. Open and copy the password from output file: `nano lndg-admin.txt` +7. Use the password from the output file and the username `lndg-admin` to login ### Updating ``` @@ -18,16 +21,12 @@ docker-compose build --no-cache docker-compose up -d ``` -### Notes -1. Unless you save your `db.sqlite3` file before destroying your container, this data will be lost and rebuilt when making a new container. However, some data such as rebalances from previous containers cannot be rebuilt. -2. You can make this file persist by initializing it first locally `touch /root/lndg/db.sqlite3` and then mapping it locally in your docker-compose file under the volumes. `/root/lndg/db.sqlite3:/lndg/db.sqlite3:rw` - ## Umbrel Installation ### 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. Initialize the database file `touch db.sqlite3` +4. Initialize db and admin backup `touch db.sqlite3 && touch lndg-admin.txt` 5. Copy and replace the contents of the docker-compose.yaml with the below: `nano docker-compose.yaml` ``` services: @@ -36,10 +35,11 @@ services: volumes: - /home/umbrel/umbrel/lnd:/root/.lnd:ro - /home/umbrel/lndg/db.sqlite3:/lndg/db.sqlite3:rw + - /home/umbrel/lndg/lndg-admin.txt:/lndg/lndg-admin.txt:rw command: - sh - -c - - python initialize.py -net 'mainnet' -server '10.21.21.9:10009' -d && python manage.py migrate && python manage.py collectstatic --no-input && supervisord && python manage.py runserver 0.0.0.0:8000 + - 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: @@ -49,6 +49,8 @@ networks: ``` 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 lndg-admin.txt` +8. Use the password from the output file and the username `lndg-admin` to login ### Updating ``` @@ -65,9 +67,10 @@ docker-compose up -d 4. Setup a python3 virtual environment `virtualenv -p python3 .venv` 5. Install required dependencies `.venv/bin/pip install -r requirements.txt` 6. Initialize some settings for your django site (see notes below) `.venv/bin/python initialize.py` -7. Migrate all database objects `.venv/bin/python manage.py migrate` +7. The initial login user is `lndg-admin` and the password is output here: `lndg-admin.txt` 8. Generate some initial data for your dashboard `.venv/bin/python jobs.py` 9. Run the server via a python development server `.venv/bin/python manage.py runserver 0.0.0.0:8889` +Tip: If you plan to only use the development server, you will need to setup whitenoise (see note below). ### Step 2 - Setup Backend Data, Automated Rebalancing and HTLC Stream Data The files `jobs.py`, `rebalancer.py` and `htlc_stream.py` inside lndg/gui/ serve to update the backend database with the most up to date information, rebalance any channels based on your lndg dashboard settings and to listen for any failure events in your htlc stream. A refresh interval of at least 10-20 seconds is recommended for the `jobs.py` and `rebalancer.py` files for the best user experience. @@ -84,11 +87,21 @@ Recommend Setup With Supervisord or Systemd Alternatively, you may also make your own task for these files with your preferred tool (task scheduler/cronjob/etc). +### Updating +1. Make sure you are in the lndg folder `cd lndg` +2. Pull the new files `git pull` +3. Migrate any database changes `.venv/bin/python manage.py migrate` + +### Nginx Webserver +If you would like to serve the dashboard at all times, it is recommended to setup a proper production webserver to host the site. +A bash script has been included to help aide in the setup of a nginx webserver. `sudo bash nginx.sh` + ### Notes 1. If you are not using the default settings for LND or you would like to run a LND instance on a network other than `mainnet` you can use the correct flags in step 6 (see `initialize.py --help`) or you can edit the variables directly in `lndg/lndg/settings.py`. 2. Some systems have a hard time serving static files (docker/macOs) and installing whitenoise and configuring it can help solve this issue. You can use `initialize.py -wn` to setup whitenoise and install it with `.venv/bin/pip install whitenoise`. 3. If you want to recreate a settings file, delete it from `lndg/lndg/settings.py` and rerun. `initialize.py` -4. If you plan to run this site continuously, consider setting up a proper web server to host it (see Nginx below). +4. If you plan to run this site continuously, consider setting up a proper web server to host it (see Nginx below). +5. You can manage your login credentials from the admin page. Example: `lndg.local/lndg-admin` ### Setup lndg initialize.py options 1. `-ip` or `--nodeip` - Accepts only this host IP to serve the LNDg page - default: `*` @@ -96,18 +109,11 @@ Alternatively, you may also make your own task for these files with your preferr 3. `-net` or `--network` - Network LND will run over - default: `mainnet` 4. `-server` or `--rpcserver` - Server address to use for rpc communications with LND - default: `localhost:10009` 5. `-sd` or `--supervisord` - Setup supervisord to run jobs/rebalancer background processes - default: `False` -6. `-wn` or `--whitenoise` - Add whitenoise middleware (docker requirement for static files) - default: `False` -7. `-d` or `--docker` - Single option for docker container setup (supervisord + whitenoise) - default: `False` -8. `-dx` or `--debug` - Setup the django site in debug mode - default: `False` - -### Updating -1. Make sure you are in the lndg folder `cd lndg` -2. Pull the new files `git pull` -3. Migrate any database changes `.venv/bin/python manage.py migrate` - -### Nginx Webserver -If you would like to serve the dashboard at all times, it is recommended to setup a proper production webserver to host the site. -A bash script has been included to help aide in the setup of a nginx webserver. `sudo bash nginx.sh` +6. `-sdu` or `--sduser` - Configure supervisord with a non-root user - default: `root` +7. `-wn` or `--whitenoise` - Add whitenoise middleware (docker requirement for static files) - default: `False` +8. `-d` or `--docker` - Single option for docker container setup (supervisord + whitenoise) - default: `False` +9. `-dx` or `--debug` - Setup the django site in debug mode - default: `False` +10. `-pw` or `--adminpw` Setup a custom admin password - default: `Randomized` ## Key Features ### API Backend @@ -117,6 +123,12 @@ The following data can be accessed at the /api endpoint: ### Peer Reconnection LNDg will automatically try to resolve any channels that are seen as inactive, no more than every 3 minutes per peer. +### Suggests New Peers +LNDg will make suggestions for new peers to open channels to based on your node's successful routing history. + +### Suggests New Peers +LNDg will make suggestions for new peers to open channels to based on your node's successful routing history. + ### HTLC Failure Stream LNDg will listen for failure events in your htlc stream and record them to the dashboard when they occur. diff --git a/docker-compose.yaml b/docker-compose.yaml index dc83efd0..87d4cfba 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,9 +3,11 @@ services: build: . volumes: - /root/.lnd:/root/.lnd:ro + - /root/lndg/db.sqlite3:/lndg/db.sqlite3:rw + - /root/lndg/lndg-admin.txt:/lndg/lndg-admin.txt:rw command: - sh - -c - - python initialize.py -net 'mainnet' -server 'localhost:10009' -d && python manage.py migrate && python manage.py collectstatic --no-input && supervisord && python manage.py runserver 0.0.0.0:8000 + - python initialize.py -net 'mainnet' -server 'localhost:10009' -d && supervisord && python manage.py runserver 0.0.0.0:8000 ports: - 8889:8000 \ No newline at end of file diff --git a/gui/forms.py b/gui/forms.py index 63c6de28..cd568966 100644 --- a/gui/forms.py +++ b/gui/forms.py @@ -38,8 +38,7 @@ class CloseChannelForm(forms.Form): force = forms.BooleanField(widget=forms.CheckboxSelectMultiple, required=False) class ConnectPeerForm(forms.Form): - peer_pubkey = forms.CharField(label='peer_pubkey', max_length=66) - host = forms.CharField(label='host', max_length=120) + peer_id = forms.CharField(label='peer_pubkey', max_length=200) class AddInvoiceForm(forms.Form): value = forms.IntegerField(label='value') diff --git a/gui/serializers.py b/gui/serializers.py index 038de7e0..18652087 100644 --- a/gui/serializers.py +++ b/gui/serializers.py @@ -55,8 +55,7 @@ class Meta: exclude = [] class ConnectPeerSerializer(serializers.Serializer): - peer_pubkey = serializers.CharField(label='peer_pubkey', max_length=66) - host = serializers.CharField(label='host', max_length=120) + peer_id = serializers.CharField(label='peer_pubkey', max_length=200) class OpenChannelSerializer(serializers.Serializer): peer_pubkey = serializers.CharField(label='peer_pubkey', max_length=66) diff --git a/gui/templates/action_list.html b/gui/templates/action_list.html new file mode 100644 index 00000000..07eee150 --- /dev/null +++ b/gui/templates/action_list.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} +{% block content %} +{% load humanize %} +{% if action_list %} +
+

Suggested Action List

+ + + + + + + + + + + + + + + + + + + {% for channel in action_list %} + + + + + + + + + + + + + + + + + + {% endfor %} +
Channel IDPeer AliasCapacityOutbound LiquidityInbound LiquidityUnsettledoRateoBaseo7Di7DiRateiBaseARAction
{{ channel.chan_id }}{{ channel.alias }}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }} ({{ channel.outbound_percent }}%)
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
{{ channel.remote_balance|intcomma }} ({{ channel.inbound_percent }}%){{ channel.unsettled_balance|intcomma }}{{ channel.local_fee_rate|intcomma }}{{ channel.local_base_fee|intcomma }}{{ channel.o7D|intcomma }} M ({{ channel.routed_out_7day }}){{ channel.i7D|intcomma }} M ({{ channel.routed_in_7day }}){{ channel.remote_fee_rate|intcomma }}{{ channel.remote_base_fee|intcomma }} + {% if channel.auto_rebalance == True %} +
+ {% csrf_token %} + + +
+ {% else %} +
+ {% csrf_token %} + + +
+ {% endif %} +
{{ channel.output }}
+
+{% endif %} +{% if not action_list %} +
+

Nothing to see here! Great job!

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/base.html b/gui/templates/base.html index 64aeaf22..6ec852dc 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -25,4 +25,11 @@

My Lnd Overview

{% block content %}{% endblock %} + \ No newline at end of file diff --git a/gui/templates/home.html b/gui/templates/home.html index 9f46f05e..80c73a7f 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -1,8 +1,8 @@ {% extends "base.html" %} {% block content %} -{% load humanize %} +{% load humanize %}
-

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

+

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

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

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

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

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

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

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

-

Suggested New Peers

+

Suggested New Peers | Suggested AR Actions

{% if active_channels %}
@@ -50,8 +50,8 @@

Active Channels

{% for channel in active_channels %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {{ channel.alias }} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} ({{ channel.outbound_percent }}%)
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
@@ -121,8 +121,8 @@

Inactive Channels

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

Pending Open Channels

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

Pending Close Channels

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

Pending Force Close Channels

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

Channels Waiting To Close

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

Connect To A Peer

{% csrf_token %} - - - - + +
diff --git a/gui/templates/testnet.html b/gui/templates/testnet.html deleted file mode 100644 index 84a5a11b..00000000 --- a/gui/templates/testnet.html +++ /dev/null @@ -1,585 +0,0 @@ -{% extends "base.html" %} -{% block content %} -{% load humanize %} -
-

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

-

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

-

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

-
-
-

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

-
- {% csrf_token %} - -
-
-
-

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

-

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

-

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

-

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

-
-
-

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

-

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

-

Suggested New Peers

-
-{% if active_channels %} -
-

Active Channels

-
- - - - - - - - - - - - - - - - - - - - - {% for channel in active_channels %} - - - - - - - - - - - - - - - - - - - - {% endfor %} -
Channel IDPeer AliasCapacityOutbound LiquidityInbound LiquidityUnsettledoRateoBaseo7Di7DoLifeiLifeiRateiBaseiTarget%AR
{{ channel.chan_id }}{{ channel.alias }}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }} ({{ channel.outbound_percent }}%)
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
{{ channel.remote_balance|intcomma }} ({{ channel.inbound_percent }}%){{ channel.unsettled_balance|intcomma }} ({{ channel.htlc_count }}){{ channel.local_fee_rate|intcomma }}{{ channel.local_base_fee|intcomma }}{{ channel.amt_routed_out_7day|intcomma }} M ({{ channel.routed_out_7day }}){{ channel.amt_routed_in_7day|intcomma }} M ({{ channel.routed_in_7day }}){{ channel.amt_routed_out|intcomma }} M ({{ channel.routed_out }}){{ channel.amt_routed_in|intcomma }} M ({{ channel.routed_in }}){{ channel.remote_fee_rate|intcomma }}{{ channel.remote_base_fee|intcomma }} - {% if channel.auto_rebalance == True %} -
- {% csrf_token %} - - -
- {% else %} - --- - {% endif %} -
- {% if channel.auto_rebalance == True %} -
- {% csrf_token %} - - -
- {% else %} -
- {% csrf_token %} - - -
- {% endif %} -
-
-
-{% endif %} -{% if inactive_channels %} -
-

Inactive Channels

- - - - - - - - - - - - - - - - - - - - {% for channel in inactive_channels %} - - - - - - - - - - - - - - - - - - - {% endfor %} -
Channel IDPeer AliasCapacityOutbound LiquidityInbound LiquidityUnsettledoRateoBaseiRateiBaseLocal CommitLocal ReserveInitiated By MeiTarget%AR
{{ channel.chan_id }}{{ channel.alias }}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }}
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
{{ channel.remote_balance|intcomma }}{{ channel.unsettled_balance|intcomma }}{{ channel.local_fee_rate|intcomma }}{{ channel.local_base_fee|intcomma }}{{ channel.remote_fee_rate|intcomma }}{{ channel.remote_base_fee|intcomma }}{{ channel.local_commit|intcomma }}{{ channel.local_chan_reserve|intcomma }}{{ channel.initiator }} - {% if channel.auto_rebalance == True %} -
- {% csrf_token %} - - -
- {% else %} - --- - {% endif %} -
- {% if channel.auto_rebalance == True %} -
- {% csrf_token %} - - -
- {% else %} -
- {% csrf_token %} - - -
- {% endif %} -
-
-{% endif %} -{% if pending_open %} -
-

Pending Open Channels

- - - - - - - - - - {% for channel in pending_open %} - - {% with pending_funding_txid=channel.channel.channel_point|slice:":-2" %} - - {% endwith %} - - - - - - - {% endfor %} -
Channel PointPeer PubKeyCapacityLocal BalanceRemote BalanceCommit Fee
{{ channel.channel.channel_point }}{{ channel.channel.remote_node_pub }}{{ channel.channel.capacity|intcomma }}{{ channel.channel.local_balance|intcomma }}{{ channel.channel.remote_balance|intcomma }}{{ channel.commit_fee }}
-
-{% endif %} -{% if pending_closed %} -
-

Pending Close Channels

- - - - - - - - - - - {% for channel in pending_closed %} - - {% with pending_funding_txid=channel.channel.channel_point|slice:":-2" %} - - {% endwith %} - - - - - - - - {% endfor %} -
Channel PointPeer PubKeyCapacityLocal BalanceRemote BalanceBalance In LimboLocal Commit Fee
{{ channel.channel.channel_point }}{{ channel.channel.remote_node_pub }}{{ channel.channel.capacity|intcomma }}{{ channel.channel.local_balance|intcomma }}{{ channel.channel.remote_balance|intcomma }}{{ channel.limbo_balance|intcomma }}{{ channel.commitments.local_commit_fee_sat }}
-
-{% endif %} -{% if pending_force_closed %} -
-

Pending Force Close Channels

- - - - - - - - - - - - {% for channel in pending_force_closed %} - - {% with pending_funding_txid=channel.channel.channel_point|slice:":-2" %} - - {% endwith %} - - - - - - - - - {% endfor %} -
Channel PointPeer PubKeyCapacityLocal BalanceRemote BalanceBalance In LimboBlocks To MaturityClosing TX
{{ channel.channel.channel_point }}{{ channel.channel.remote_node_pub }}{{ channel.channel.capacity|intcomma }}{{ channel.channel.local_balance|intcomma }}{{ channel.channel.remote_balance|intcomma }}{{ channel.limbo_balance|intcomma }}{{ channel.blocks_til_maturity|intcomma }}{{ channel.closing_txid }}
-
-{% endif %} -{% if waiting_for_close %} -
-

Channels Waiting To Close

- - - - - - - - - - - {% for channel in waiting_for_close %} - - {% with pending_funding_txid=channel.channel.channel_point|slice:":-2" %} - - {% endwith %} - - - - - - - - {% endfor %} -
Channel PointPeer PubKeyCapacityLocal BalanceRemote BalanceBalance In LimboLocal Commit Fee
{{ channel.channel.channel_point }}{{ channel.channel.remote_node_pub }}{{ channel.channel.capacity|intcomma }}{{ channel.channel.local_balance|intcomma }}{{ channel.channel.remote_balance|intcomma }}{{ channel.limbo_balance|intcomma }}{{ channel.commitments.local_commit_fee_sat }}
-
-{% endif %} -{% if forwards %} -
-

Last 10 Payments Routed

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

Last 10 Rebalance Requests

- - - - - - - - - - - - {% for rebalance in rebalances %} - - - - - - - - - - - {% endfor %} -
RequestedStartStopScheduled DurationValueFee LimitLast Hop AliasStatus
{{ rebalance.requested|naturaltime }}{% if rebalance.status == 0 %}N/A{% else %}{{ rebalance.start|naturaltime }}{% endif %}{% if rebalance.status > 1 %}{{ rebalance.stop|naturaltime }}{% else %}N/A{% endif %}{{ rebalance.duration }} minutes{{ rebalance.value|intcomma }}{{ rebalance.fee_limit|intcomma }}{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}Successful{% elif rebalance.status == 3 %}Timeout{% elif rebalance.status == 4 %}No Route{% elif rebalance.status == 5 %}Error{% elif rebalance.status == 6 %}Incorrect Payment Details{% elif rebalance.status == 7 %}Insufficient Balance{% elif rebalance.status == 400 %}Rebalancer Request Failed{% elif rebalance.status == 408 %}Rebalancer Request Timeout{% else %}{{ rebalance.status }}{% endif %}
-
-{% endif %} -{% if payments %} -
-

Last 5 Payments Sent

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

Last 5 Payments Received

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

Last 10 Failed HTLCs

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

Auto-Rebalancer Settings

- - - - - - {% for settings in local_settings %} - - - - - {% endfor %} -
KeyValue
{{ settings.key }}{{ settings.value|intcomma }}
-
-{% endif %} -
-

Update Auto Rebalancer Settings

-
-
- {% csrf_token %} - - - - - - - - - - - - - -
-
-
-
-

Manual Rebalancer Request

-
-
- {% csrf_token %} - - - - - - - - - -
    -
  • - {% for channel in rebalancer_form.outgoing_chan_ids %} -
  • {{ channel }}
  • - {% endfor %} -
-
-
-
-
-

Connect To A Peer

-
-
- {% csrf_token %} - - - - - -
-
-
-
-

Open A Channel

-
-
- {% csrf_token %} - - - - - - - -
-
-
-
-

Close A Channel

-
-
- {% csrf_token %} - - - - - - - -
-
-
-
-

Create Invoice

-
-
- {% csrf_token %} - - - -
-
-
-
-

Update Channel Policy

-
-
- {% csrf_token %} - - - - - - - -
    -
  • - {% for channel in chan_policy_form.target_chans %} -
  • {{ channel }}
  • - {% endfor %} -
-
-
-
-
-

Update Peer Alias

-
-
- {% csrf_token %} - - - -
-
-
-{% endblock %} \ No newline at end of file diff --git a/gui/urls.py b/gui/urls.py index 694b5360..1535b082 100644 --- a/gui/urls.py +++ b/gui/urls.py @@ -1,5 +1,6 @@ from django.urls import path, include from rest_framework import routers +from django.contrib import admin from . import views router = routers.DefaultRouter() @@ -31,11 +32,14 @@ path('autorebalance/', views.auto_rebalance, name='auto-rebalance'), path('ar_target/', views.ar_target, name='ar-target'), path('suggested_opens/', views.suggested_opens, name='suggested-opens'), + path('suggested_actions/', views.suggested_actions, name='suggested-actions'), path('api/', include(router.urls), name='api-root'), + path('api-auth/', include('rest_framework.urls'), name='api-auth'), path('api/connectpeer/', views.connect_peer, name='connect-peer'), path('api/openchannel/', views.open_channel, name='open-channel'), path('api/closechannel/', views.close_channel, name='close-channel'), path('api/createinvoice/', views.add_invoice, name='add-invoice'), path('api/newaddress/', views.new_address, name='new-address'), path('api/updatealias/', views.update_alias, name='update-alias'), + path('lndg-admin/', admin.site.urls), ] diff --git a/gui/views.py b/gui/views.py index aead5c32..d9c85b42 100644 --- a/gui/views.py +++ b/gui/views.py @@ -2,6 +2,7 @@ from django.shortcuts import get_object_or_404, render, redirect from django.db.models import Sum, IntegerField, Count from django.db.models.functions import Round +from django.contrib.auth.decorators import login_required from django.conf import settings from datetime import datetime, timedelta from rest_framework import viewsets @@ -15,7 +16,7 @@ from .lnd_deps.lnd_connect import lnd_connect from lndg.settings import LND_NETWORK -# Create your views here. +@login_required(login_url='/lndg-admin/login/?next=/') def home(request): if request.method == 'GET': stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) @@ -149,15 +150,14 @@ def home(request): 'routed_ppm': 0 if total_value_forwards == 0 else int((total_earned/total_value_forwards)*1000000), '7day_routed_ppm': 0 if routed_7day_amt == 0 else int((total_earned_7day/routed_7day_amt)*1000000), '7day_payments_ppm': 0 if payments_7day_amt == 0 else int((total_7day_fees/payments_7day_amt)*1000000), - 'liq_ratio': 0 if total_outbound == 0 else int((total_inbound/sum_outbound)*100) + 'liq_ratio': 0 if total_outbound == 0 else int((total_inbound/sum_outbound)*100), + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '' } - if LND_NETWORK == 'testnet': - return render(request, 'testnet.html', context) - else: - return render(request, 'home.html', context) + return render(request, 'home.html', context) else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def route(request): if request.method == 'GET': payment_hash = request.GET.urlencode()[1:] @@ -169,6 +169,7 @@ def route(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def peers(request): if request.method == 'GET': context = { @@ -178,6 +179,7 @@ def peers(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def balances(request): if request.method == 'GET': stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) @@ -189,6 +191,7 @@ def balances(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def suggested_opens(request): if request.method == 'GET': stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) @@ -204,6 +207,60 @@ def suggested_opens(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') +def suggested_actions(request): + if request.method == 'GET': + channels = Channels.objects.filter(is_active=True, is_open=True).annotate(outbound_percent=(Sum('local_balance')*1000)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*1000)/Sum('capacity')) + filter_7day = datetime.now() - timedelta(days=7) + forwards = Forwards.objects.filter(forward_date__gte=filter_7day) + action_list = [] + for channel in channels: + result = {} + result['chan_id'] = channel.chan_id + result['alias'] = channel.alias + result['capacity'] = channel.capacity + result['local_balance'] = channel.local_balance + result['remote_balance'] = channel.remote_balance + result['outbound_percent'] = int(round(channel.outbound_percent/10, 0)) + result['inbound_percent'] = int(round(channel.inbound_percent/10, 0)) + result['unsettled_balance'] = channel.unsettled_balance + result['local_base_fee'] = channel.local_base_fee + result['local_fee_rate'] = channel.local_fee_rate + result['remote_base_fee'] = channel.remote_base_fee + result['remote_fee_rate'] = channel.remote_fee_rate + result['routed_in_7day'] = forwards.filter(chan_id_in=channel.chan_id).count() + result['routed_out_7day'] = forwards.filter(chan_id_out=channel.chan_id).count() + result['i7D'] = 0 if result['routed_in_7day'] == 0 else int(forwards.filter(chan_id_in=channel.chan_id).aggregate(Sum('amt_in_msat'))['amt_in_msat__sum']/10000000)/100 + result['o7D'] = 0 if result['routed_out_7day'] == 0 else int(forwards.filter(chan_id_out=channel.chan_id).aggregate(Sum('amt_out_msat'))['amt_out_msat__sum']/10000000)/100 + result['auto_rebalance'] = channel.auto_rebalance + result['ar_target'] = channel.ar_target + if result['o7D'] > (result['i7D']*1.10) and result['outbound_percent'] > 75: + print('Case 1: Pass') + continue + elif result['o7D'] > (result['i7D']*1.10) and result['inbound_percent'] > 75 and channel.auto_rebalance == False: + print('Case 2: Enable AR') + result['output'] = 'Enable AR' + result['reason'] = 'o7D > i7D AND Inbound Liq > 75%' + elif result['o7D'] < (result['i7D']*1.10) and result['outbound_percent'] > 75 and channel.auto_rebalance == True: + print('Case 3: Disable AR') + result['output'] = 'Disable AR' + result['reason'] = 'o7D < i7D AND Outbound Liq > 75%' + elif result['o7D'] < (result['i7D']*1.10) and result['inbound_percent'] > 75: + print('Case 4: Pass') + continue + else: + print('Case 5: Pass') + continue + if len(result) > 0: + action_list.append(result) + context = { + 'action_list': action_list + } + return render(request, 'action_list.html', context) + else: + return redirect('home') + +@login_required(login_url='/lndg-admin/login/?next=/') def pending_htlcs(request): if request.method == 'GET': context = { @@ -214,16 +271,34 @@ def pending_htlcs(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def open_channel_form(request): if request.method == 'POST': form = OpenChannelForm(request.POST) if form.is_valid(): try: stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - pubkey_bytes = bytes.fromhex(form.cleaned_data['peer_pubkey']) - for response in stub.OpenChannel(ln.OpenChannelRequest(node_pubkey=pubkey_bytes, local_funding_amount=form.cleaned_data['local_amt'], sat_per_byte=form.cleaned_data['sat_per_byte'])): - messages.success(request, 'Channel created! Funding TXID: ' + str(response.chan_pending.txid[::-1].hex()) + ':' + str(response.chan_pending.output_index)) - break + peer_pubkey = form.cleaned_data['peer_pubkey'] + connected = False + if Peers.objects.filter(pubkey=peer_pubkey, connected=True).exists(): + connected = True + else: + try: + node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node + host = node.addresses[0].addr + ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host) + response = stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr)) + connected = True + 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] + messages.error(request, 'Error connecting to new peer: ' + error_msg) + if connected: + for response in stub.OpenChannel(ln.OpenChannelRequest(node_pubkey=bytes.fromhex(peer_pubkey), local_funding_amount=form.cleaned_data['local_amt'], sat_per_byte=form.cleaned_data['sat_per_byte'])): + messages.success(request, 'Channel created! Funding TXID: ' + str(response.chan_pending.txid[::-1].hex()) + ':' + str(response.chan_pending.output_index)) + break except Exception as e: error = str(e) details_index = error.find('details =') + 11 @@ -234,6 +309,7 @@ def open_channel_form(request): messages.error(request, 'Invalid Request. Please try again.') return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def close_channel_form(request): if request.method == 'POST': form = CloseChannelForm(request.POST) @@ -262,31 +338,44 @@ def close_channel_form(request): messages.error(request, 'Channel ID is not valid. Please try again.') except Exception as e: error = str(e) - messages.error(request, 'Channel close failed! Error: ' + error) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + messages.error(request, 'Channel close failed! Error: ' + error_msg) else: messages.error(request, 'Invalid Request. Please try again.') return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def connect_peer_form(request): if request.method == 'POST': form = ConnectPeerForm(request.POST) if form.is_valid(): try: stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - peer_pubkey = form.cleaned_data['peer_pubkey'] - host = form.cleaned_data['host'] - ln_addr = ln.LightningAddress() - ln_addr.pubkey = peer_pubkey - ln_addr.host = host + peer_id = form.cleaned_data['peer_id'] + if peer_id.count('@') == 0 and len(peer_id) == 66: + peer_pubkey = peer_id + node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node + host = node.addresses[0].addr + elif peer_id.count('@') == 1 and len(peer_id.split('@')[0]) == 66: + peer_pubkey, host = peer_id.split('@') + else: + raise Exception('Invalid peer pubkey or connection string.') + ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host) response = stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr)) - messages.success(request, 'Connection successful! ' + str(response)) + messages.success(request, 'Connection successful!' + str(response)) except Exception as e: error = str(e) - messages.error(request, 'Connection request failed! Error: ' + error) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + messages.error(request, 'Connection request failed! Error: ' + error_msg) else: messages.error(request, 'Invalid Request. Please try again.') return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def new_address_form(request): if request.method == 'POST': try: @@ -295,9 +384,13 @@ def new_address_form(request): messages.success(request, 'Deposit Address: ' + str(response.address)) except Exception as e: error = str(e) - messages.error(request, 'Address request! Error: ' + error) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + messages.error(request, 'Address request failed! Error: ' + error_msg) return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def add_invoice_form(request): if request.method == 'POST': form = AddInvoiceForm(request.POST) @@ -308,28 +401,40 @@ def add_invoice_form(request): messages.success(request, 'Invoice created! ' + str(response.payment_request)) except Exception as e: error = str(e) - messages.error(request, 'Invoice creation failed! Error: ' + error) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + messages.error(request, 'Invoice creation failed! Error: ' + error_msg) else: messages.error(request, 'Invalid Request. Please try again.') return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def rebalance(request): if request.method == 'POST': form = RebalancerForm(request.POST) if form.is_valid(): try: - chan_ids = [] - for channel in form.cleaned_data['outgoing_chan_ids']: - chan_ids.append(channel.chan_id) - Rebalancer(value=form.cleaned_data['value'], fee_limit=form.cleaned_data['fee_limit'], outgoing_chan_ids=chan_ids, last_hop_pubkey=form.cleaned_data['last_hop_pubkey'], duration=form.cleaned_data['duration']).save() - messages.success(request, 'Rebalancer request created!') + if Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey']).exists(): + chan_ids = [] + for channel in form.cleaned_data['outgoing_chan_ids']: + chan_ids.append(channel.chan_id) + target_alias = Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey'])[0].alias if Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey']).exists() else None + Rebalancer(value=form.cleaned_data['value'], fee_limit=form.cleaned_data['fee_limit'], outgoing_chan_ids=chan_ids, last_hop_pubkey=form.cleaned_data['last_hop_pubkey'], target_alias=target_alias, duration=form.cleaned_data['duration']).save() + messages.success(request, 'Rebalancer request created!') + else: + messages.error(request, 'Target peer is invalid or unknown.') except Exception as e: error = str(e) - messages.error(request, 'Error entering rebalancer request! Error: ' + error) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + messages.error(request, 'Error entering rebalancer request! Error: ' + error_msg) else: messages.error(request, 'Invalid Request. Please try again.') return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def update_chan_policy(request): if request.method == 'POST': form = ChanPolicyForm(request.POST) @@ -351,12 +456,16 @@ def update_chan_policy(request): messages.success(request, 'Channel policies updated! This will be broadcast during the next graph update!') except Exception as e: error = str(e) - messages.error(request, 'Error updating channel policies! Error: ' + error) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + messages.error(request, 'Error updating channel policies! Error: ' + error_msg) else: messages.error(request, 'Invalid Request. Please try again.') print(form.errors) return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def auto_rebalance(request): if request.method == 'POST': form = AutoRebalanceForm(request.POST) @@ -435,6 +544,7 @@ def auto_rebalance(request): messages.error(request, 'Invalid Request. Please try again.') return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') def ar_target(request): if request.method == 'POST': form = ARTarget(request.POST) @@ -526,16 +636,24 @@ def connect_peer(request): if serializer.is_valid(): try: stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - peer_pubkey = serializer.validated_data['peer_pubkey'] - host = serializer.validated_data['host'] - ln_addr = ln.LightningAddress() - ln_addr.pubkey = peer_pubkey - ln_addr.host = host + peer_id = serializer.validated_data['peer_id'] + if peer_id.count('@') == 0 and len(peer_id) == 66: + peer_pubkey = peer_id + node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node + host = node.addresses[0].addr + elif peer_id.count('@') == 1 and len(peer_id.split('@')[0]) == 66: + peer_pubkey, host = peer_id.split('@') + else: + raise Exception('Invalid peer pubkey or connection string.') + ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host) response = stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr)) - return Response({'message': 'Connection successful! ' + str(response)}) + return Response({'message': 'Connection successful!' + str(response)}) except Exception as e: error = str(e) - return Response({'error': 'Connection request failed! Error: ' + error}) + 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': 'Connection request failed! Error: ' + error_msg}) else: return Response({'error': 'Invalid request!'}) @@ -545,9 +663,26 @@ def open_channel(request): if serializer.is_valid(): try: stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - pubkey_bytes = bytes.fromhex(serializer.validated_data['peer_pubkey']) - for response in stub.OpenChannel(ln.OpenChannelRequest(node_pubkey=pubkey_bytes, local_funding_amount=serializer.validated_data['local_amt'], sat_per_byte=serializer.validated_data['sat_per_byte'])): - return Response({'message': 'Channel created! Funding TXID: ' + str(response.chan_pending.txid[::-1].hex()) + ':' + str(response.chan_pending.output_index)}) + peer_pubkey = serializer.validated_data['peer_pubkey'] + connected = False + if Peers.objects.filter(pubkey=peer_pubkey, connected=True).exists(): + connected = True + else: + try: + node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer_pubkey, include_channels=False)).node + host = node.addresses[0].addr + ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host) + response = stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr)) + connected = True + except Exception as e: + error = str(e) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + return Response({'error': 'Error connecting to new peer: ' + error_msg}) + if connected: + for response in stub.OpenChannel(ln.OpenChannelRequest(node_pubkey=bytes.fromhex(peer_pubkey), local_funding_amount=serializer.validated_data['local_amt'], sat_per_byte=serializer.validated_data['sat_per_byte'])): + return Response({'message': 'Channel created! Funding TXID: ' + str(response.chan_pending.txid[::-1].hex()) + ':' + str(response.chan_pending.output_index)}) except Exception as e: error = str(e) details_index = error.find('details =') + 11 @@ -583,7 +718,10 @@ def close_channel(request): return Response({'error': 'Channel ID is not valid.'}) except Exception as e: error = str(e) - return Response({'error': 'Channel close failed! Error: ' + error}) + 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': 'Channel close failed! Error: ' + error_msg}) else: return Response({'error': 'Invalid request!'}) @@ -597,7 +735,10 @@ def add_invoice(request): return Response({'message': 'Invoice created!', 'data':str(response.payment_request)}) except Exception as e: error = str(e) - return Response({'error': 'Invoice creation failed! Error: ' + error}) + 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': 'Invoice creation failed! Error: ' + error_msg}) else: return Response({'error': 'Invalid request!'}) @@ -609,7 +750,10 @@ def new_address(request): return Response({'message': 'Retrieved new deposit address!', 'data':str(response.address)}) except Exception as e: error = str(e) - return Response({'error': 'Address creation failed! Error: ' + error}) + 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': 'Address creation failed! Error: ' + error_msg}) @api_view(['POST']) def update_alias(request): @@ -627,7 +771,10 @@ def update_alias(request): messages.success(request, 'Alias updated to: ' + str(new_alias)) except Exception as e: error = str(e) - messages.error(request, 'Error updating alias: ' + error) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + messages.error(request, 'Error updating alias: ' + error_msg) else: messages.error(request, 'Pubkey not in channels list.') else: diff --git a/initialize.py b/initialize.py index 455ac106..cb983a24 100644 --- a/initialize.py +++ b/initialize.py @@ -1,5 +1,9 @@ -import secrets, argparse +import secrets, argparse, django, os from pathlib import Path +from django.core.management import call_command +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): #Generate a unique secret to be used for your django site @@ -50,6 +54,7 @@ def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenois 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.admin', 'django.contrib.humanize', 'gui', 'rest_framework', @@ -118,7 +123,14 @@ def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenois REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - 'PAGE_SIZE': 100 + 'PAGE_SIZE': 100, + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ], } # Internationalization @@ -153,11 +165,10 @@ def write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenois except Exception as e: print('Error creating the settings file:', e) -def write_supervisord_settings(): - target_path = Path(__file__).resolve().parent +def write_supervisord_settings(sduser): supervisord_secret = secrets.token_urlsafe(16) supervisord_settings_file = '''[supervisord] -user=root +user=%s childlogdir = /var/log logfile = /var/log/supervisord.log logfile_maxbytes = 50MB @@ -210,7 +221,7 @@ def write_supervisord_settings(): stdout_logfile = /var/log/lndg-htlc-stream.log stdout_logfile_maxbytes = 150MB stdout_logfile_backups = 15 -''' % (supervisord_secret, supervisord_secret, target_path, target_path, target_path) +''' % (sduser, supervisord_secret, supervisord_secret, BASE_DIR, BASE_DIR, BASE_DIR) try: f = open("/usr/local/etc/supervisord.conf", "x") f.close() @@ -222,7 +233,7 @@ def write_supervisord_settings(): f.write(supervisord_settings_file) f.close() except Exception as e: - print('Error creating the settings file:', e) + print('Error creating the settings file:', str(e)) def main(): help_msg = "LNDg Settings Initializer" @@ -232,25 +243,73 @@ def main(): parser.add_argument('-net', '--network', help = 'Network LND will run over', default='mainnet') parser.add_argument('-server', '--rpcserver', help = 'Server address to use for rpc communications with LND', default='localhost:10009') parser.add_argument('-sd', '--supervisord', help = 'Setup supervisord to run jobs/rebalancer background processes', action='store_true') + parser.add_argument('-sdu', '--sduser', help = 'Configure supervisord with a non-root user', default='root') 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('-pw', '--adminpw', help = 'Setup a custom admin password', default=None) args = parser.parse_args() node_ip = args.nodeip lnd_dir_path = args.lnddir lnd_network = args.network lnd_rpc_server = args.rpcserver setup_supervisord = args.supervisord + sduser = args.sduser whitenoise = args.whitenoise docker = args.docker debug = args.debug + adminpw = args.adminpw if docker: setup_supervisord = True whitenoise = True write_settings(node_ip, lnd_dir_path, lnd_network, lnd_rpc_server, whitenoise, debug) if setup_supervisord: print('Supervisord setup requested...') - write_supervisord_settings() + write_supervisord_settings(sduser) + try: + settings.configure( + SECRET_KEY = secrets.token_urlsafe(64), + DATABASES = { + 'default':{ + 'ENGINE':'django.db.backends.sqlite3', + 'NAME':BASE_DIR/'db.sqlite3' + } + }, + INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + 'gui', + ], + STATIC_URL = 'static/', + STATIC_ROOT = os.path.join(BASE_DIR, 'gui/static/') + ) + django.setup() + call_command('migrate', verbosity=0) + call_command('collectstatic', verbosity=0, interactive=False) + 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') + login_pw = secrets.token_urlsafe(16) if adminpw is None else adminpw + admin.set_password(login_pw) + admin.save() + if adminpw is None: + try: + f = open("lndg-admin.txt", "w") + f.write(login_pw) + f.close() + except Exception as e: + print('Error writing password file:', str(e)) + print('FIRST TIME LOGIN PASSWORD:' + login_pw) + except Exception as e: + print('Error setting up initial user:', str(e)) + except Exception as e: + print('Error initializing django:', str(e)) if __name__ == '__main__': main() \ No newline at end of file