diff --git a/README.md b/README.md
index fa4d13e2..38d4d41b 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,30 @@
-# LNDg
-Lite GUI web interface to analyze lnd data and manage your node with automation.
+# LNDg - GUI for LND Data Analysis and Node Management
-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)
+Welcome to LNDg, an advanced web interface designed for analyzing LND data and automating node management tasks.
-LNDg can also be found directly on popular apps like Umbrel and Citadel with a 1-click install from the GUI.
+Choose your preferred installation method:
-## Docker Installation (requires docker and docker-compose be installed)
-### Build and deploy
-1. Clone respository `git clone https://github.com/cryptosharks131/lndg.git`
-2. Change directory into the repo `cd lndg`
-3. Copy and replace the contents (adjust custom volume paths to LND and LNDg folders) of the `docker-compose.yaml` with the below: `nano docker-compose.yaml`
+- **1-Click Installation**: Easily install LNDg directly from popular platforms like Umbrel, Citadel, Start9 and RaspiBlitz.
+- [Docker Installation](https://github.com/cryptosharks131/lndg#docker-installation-requires-docker-and-docker-compose-be-installed): Ideal for users familiar with Docker and Docker Compose.
+- [Manual Installation](https://github.com/cryptosharks131/lndg#manual-installation): If you prefer a hands-on approach to set up LNDg.
+
+
+## Docker Installation (requires Docker and Docker Compose)
+
+### Prepare Install
+
+```bash
+# Clone the repository
+git clone https://github.com/cryptosharks131/lndg.git
+
+# Change directory to the repository
+cd lndg
+
+# Customize the docker-compose.yaml file
+nano docker-compose.yaml
```
+**Replace the contents of `docker-compose.yaml` with your desired volume paths and settings. An example configuration is shown below.**
+```yaml
services:
lndg:
build: .
@@ -23,13 +37,19 @@ services:
- python initialize.py -net 'mainnet' -server '127.0.0.1:10009' -d && supervisord && python manage.py runserver 0.0.0.0:8889
network_mode: "host"
```
-4. Deploy your docker image: `docker-compose up -d`
-5. LNDg should now be available on port `http://localhost:8889`
-6. Open and copy the password from output file: `nano data/lndg-admin.txt`
-7. Use the password from the output file and the username `lndg-admin` to login
+### Build and Deploy
+```bash
+# Deploy the Docker image
+docker-compose up -d
-### Updating
+# Retrieve the admin password for login
+nano data/lndg-admin.txt
```
+- **This example configuration will host LNDg at http://0.0.0.0:8889. Use the machine IP to reach the LNDg instance.**
+- **Log in to LNDg using the provided password and the username `lndg-admin`.**
+
+### Updating
+```bash
docker-compose down
docker-compose build --no-cache
docker-compose up -d
@@ -37,49 +57,55 @@ 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`
-2. Change directory into the repo `cd lndg`
-3. Make sure you have python virtualenv installed `sudo apt install virtualenv`
-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. The initial login user is `lndg-admin` and the password is output here: `data/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.
-
-Recommend Setup With Supervisord (least setup) or Systemd (most compatible)
-1. Supervisord
- a) Setup supervisord config `.venv/bin/python initialize.py -sd`
- b) Install Supervisord `.venv/bin/pip install supervisor`
- c) Start Supervisord `supervisord`
-
-2. Systemd (2 options)
- Option 1 - Bash script install (requires install at ~/lndg) `sudo bash systemd.sh`
- Option 2 - [Manual Setup Instructions](https://github.com/cryptosharks131/lndg/blob/master/systemd.md)
-
-Alternatively, you may also make your own task for these files with your preferred tool (task scheduler/cronjob/etc).
+
+### Step 1 - Install LNDg
+
+1. Clone the repository: `git clone https://github.com/cryptosharks131/lndg.git`
+2. Change the directory into the repository: `cd lndg`
+3. Ensure you have Python virtualenv installed: `sudo apt install virtualenv`
+4. Set up a Python 3 virtual environment: `virtualenv -p python3 .venv`
+5. Install the required dependencies: `.venv/bin/pip install -r requirements.txt`
+6. Initialize necessary settings for your Django site (refer to notes below): `.venv/bin/python initialize.py`
+7. The initial login user is `lndg-admin`, and the password can be found here: `data/lndg-admin.txt`
+8. Generate initial data for your dashboard: `.venv/bin/python jobs.py`
+9. Run the server using a Python development server: `.venv/bin/python manage.py runserver 0.0.0.0:8889`
+
+*Note: If you plan to use the development server exclusively, you will need to set up whitenoise (see note below).*
+
+### Step 2 - Setup Backend Controller For Data, Automated Rebalancing, and HTLC Stream Data
+
+The file `controller.py` inside the `lndg/gui/` directory orchastrates the services needed to update the backend database with the most up-to-date information, rebalance any channels based on your LNDg dashboard settings, and listen for any failure events in your HTLC stream.
+
+**Recommended Setup with Supervisord (least setup) or Systemd (most compatible):**
+
+1. **Systemd (2 options)**
+ - Option 1 - Bash script install: `sudo bash systemd.sh`
+ - Option 2 - [Manual Setup Instructions](https://github.com/cryptosharks131/lndg/blob/master/systemd.md)
+
+2. **Supervisord**
+ - Configure Supervisord by running: `.venv/bin/python initialize.py -sd`
+ - Install Supervisord: `.venv/bin/pip install supervisor`
+ - Start Supervisord: `supervisord`
+
+
+Alternatively, you may create your own task for these files using your preferred tool (task scheduler, cron job, 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`
+
+1. Make sure you are in the LNDg folder: `cd lndg`
+2. Pull the new files from the repository: `git pull`
+3. Migrate any database changes: `.venv/bin/python manage.py migrate`
### 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 the following to install and setup whitenoise:
- `.venv/bin/pip install whitenoise && rm lndg/settings.py && .venv/bin/python initialize.py -wn`
-4. If you want to recreate a settings file, delete it from `lndg/lndg/settings.py` and rerun. `initialize.py`
-5. If you plan to run this site continuously, consider setting up a proper web server to host it (see Nginx below).
-6. You can manage your login credentials from the admin page. Example: `lndg.local/lndg-admin`
-7. If you have issues reaching the site, verify the firewall is open on port 8889 where LNDg is running
+
+1. If not using default settings for LND or would like to run on a network other than `mainnet`, use the correct flags in step 6 (see `initialize.py --help`) or edit the variables directly in `lndg/lndg/settings.py`.
+2. You can not run the development server outside of DEBUG mode due to static file issues. To address this, install and configure Whitenoise by running the following command:
+```.venv/bin/pip install whitenoise && rm lndg/settings.py && .venv/bin/python initialize.py -wn```
+3. If you need 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, it's advisable to set up a proper web server to host it (see Nginx below).
+5. You can manage your login credentials from the admin page, accessible at `/lndg-admin`.
+6. If you encounter issues accessing the site, ensure that any firewall is open on port 8889, where LNDg is running.
### Setup lndg initialize.py options
1. `-ip` or `--nodeip` - Accepts only this host IP to serve the LNDg page - default: `*`
@@ -93,37 +119,50 @@ Alternatively, you may also make your own task for these files with your preferr
9. `-d` or `--docker` - Single option for docker container setup (supervisord + whitenoise) - default: `False`
10. `-dx` or `--debug` - Setup the django site in debug mode - default: `False`
11. `-u` or `--adminuser` Setup a custom admin username - default: `lndg-admin`
-10. `-pw` or `--adminpw` Setup a custom admin password - default: `Randomized`
-10. `-csrf` or `--csrftrusted` Set trusted CSRF origins - default: `None`
-10. `-tls` or `--tlscert` Set a custom path to the tls cert - default: `--lnddir used`
-10. `-mcrn` or `--macaroon` Set a custom path to the macroon file - default: `--lnddir used`
-10. `-lnddb` or `--lnddatabase` Set a custom path to the channel.db for monitoring - default: `--lnddir used`
-10. `-nologin` or `--nologinrequired` Remove authentication requirements from LNDg - default: `False`
+12. `-pw` or `--adminpw` Setup a custom admin password - default: `Randomized`
+13. `-csrf` or `--csrftrusted` Set trusted CSRF origins - default: `None`
+14. `-tls` or `--tlscert` Set a custom path to the tls cert - default: `--lnddir used`
+15. `-mcrn` or `--macaroon` Set a custom path to the macroon file - default: `--lnddir used`
+16. `-lnddb` or `--lnddatabase` Set a custom path to the channel.db for monitoring - default: `--lnddir used`
+17. `-nologin` or `--nologinrequired` Remove authentication requirements from LNDg - default: `False`
+18. `-f` or `--force` Force the replacement of an existing settings file - default: `False`
### Using A Webserver
-You can serve the dashboard at all times using a webserver instead of the development server. Using a webserver will serve your static files and installing whitenoise is not required when running in this manner. Any webserver can be used to host the site if configured properly. A bash script has been included to help aide in the setup of a nginx webserver.
-`sudo bash nginx.sh`
+You can serve the dashboard at all times using a webserver instead of the development server. Using a webserver will serve your static files, and installing whitenoise is not required when running in this manner. Any webserver can be used to host the site if configured properly. A bash script has been included to help aid in the setup of an nginx webserver.
+
+To set up the nginx webserver, run the following command:
+
+```bash
+sudo bash nginx.sh
+```
+### When updating
+When updating your LNDg installation, follow the same steps as described above. However, after updating, you will also need to restart the uWSGI service to apply the changes to the user interface (UI).
-When updating, follow the same steps as above. You will also need to restart the uwsgi service for changes to take affect on the UI.
-`sudo systemctl restart uwsgi.service`
+To restart the uWSGI service, use the following command:
+
+```bash
+sudo systemctl restart uwsgi.service
+```
## Key Features
+
### Track Peer Events
LNDg will track the changes your peers make to channel policies you have in open channels and any connection events that may happen with those channels.
### Batch Opens
-You can use LNDg to batch open up to 10 channels at a time with a single transaction. This can help to signicantly reduce the channel open fees incurred when opening multiple channels.
+You can use LNDg to batch open up to 10 channels at a time with a single transaction. This can help to significantly reduce the channel open fees incurred when opening multiple channels.
### Watch Tower Management
-You can use LNDg to add, monitor or remove watch towers from the lnd node.
+You can use LNDg to add, monitor, or remove watch towers from the LND node.
### Suggests Fee Rates
LNDg will make suggestions on an adjustment to the current set outbound fee rate for each channel. This uses historical payment and forwarding data over the last 7 days to drive suggestions. You can use the Auto-Fees feature in order to automatically act upon the suggestions given.
-You may see another adjustment right after setting the new suggested fee rate on some channels. This is normal and you should wait ~24 hours before changing the fee rate again on any given channel.
+You may see another adjustment right after setting the new suggested fee rate on some channels. This is normal, and you should wait ~24 hours before changing the fee rate again on any given channel.
### Suggests New Peers
-LNDg will make suggestions for new peers to open channels to based on your node's successful routing history.
+LNDg will make suggestions for new peers to open channels to based on your node's successful routing history.
+
#### There are two unique values in LNDg:
1. Volume Score - A score based upon both the count of transactions and the volume of transactions routed through the peer
2. Savings By Volume (ppm) - The amount of sats you could have saved during rebalances if you were peered directly with this node over the total amount routed through the peer
@@ -131,7 +170,7 @@ LNDg will make suggestions for new peers to open channels to based on your node'
### Channel Performance Metrics
#### LNDg will aggregate your payment and forwarding data to provide the following metrics:
1. Outbound Flow Details - This shows the amount routed outbound next to the amount rebalanced in
-2. Revenue Details - This shows the revenue earned on the left, the profit (revenue - cost) in the middle and the assisted revenue (amount earned due to this channel's inbound flow) on the right
+2. Revenue Details - This shows the revenue earned on the left, the profit (revenue - cost) in the middle, and the assisted revenue (amount earned due to this channel's inbound flow) on the right
3. Inbound Flow Details - This shows the amount routed inbound next to the amount rebalanced out
4. Updates - This is the number of updates the channel has had and is directly correlated to the space it takes up in channel.db
@@ -179,7 +218,7 @@ AF Notes:
## Auto-Rebalancer - [Quick Start Guide](https://github.com/cryptosharks131/lndg/blob/master/quickstart.md)
### Here are some additional notes to help you better understand the Auto-Rebalancer (AR).
-The objective of the Auto-Rebalancer is to "refill" the liquidity on the local side (i.e. OUTBOUND) of profitable and lucarative channels. So that, when a forward comes in from another node there is always enough liquidity to route the payment and in return collect the desired routing fees.
+The objective of the Auto-Rebalancer is to "refill" the liquidity on the local side (i.e. OUTBOUND) of profitable and lucrative channels. So that, when a forward comes in from another node there is always enough liquidity to route the payment and in return collect the desired routing fees.
1. The AR variable `AR-Enabled` must be set to 1 (enabled) in order to start looking for new rebalance opportunities. (default=0)
2. The AR variable `AR-Target%` defines the % size of the channel capacity you would like to use for rebalance attempts. Example: If a channel size is 1M Sats and AR-Target% = 0.05 LNDg will select an amount of 5% of 1M = 50K for rebalancing. (default=5)
diff --git a/gui/af.py b/af.py
similarity index 84%
rename from gui/af.py
rename to af.py
index bc21b595..84164608 100644
--- a/gui/af.py
+++ b/af.py
@@ -45,7 +45,7 @@ def main(channels):
if LocalSettings.objects.filter(key='AF-LowLiqLimit').exists():
lowliq_limit = int(LocalSettings.objects.filter(key='AF-LowLiqLimit').get().value)
else:
- LocalSettings(key='AF-LowLiqLimit', value='5').save()
+ LocalSettings(key='AF-LowLiqLimit', value='15').save()
lowliq_limit = 5
if LocalSettings.objects.filter(key='AF-ExcessLimit').exists():
excess_limit = int(LocalSettings.objects.filter(key='AF-ExcessLimit').get().value)
@@ -57,12 +57,20 @@ def main(channels):
lowliq_limit = 5
excess_limit = 95
forwards = Forwards.objects.filter(forward_date__gte=filter_7day, amt_out_msat__gte=1000000)
- if forwards.exists():
- forwards_df_in_7d_sum = DataFrame.from_records(forwards.values('chan_id_in').annotate(amt_out_msat=Sum('amt_out_msat'), fee=Sum('fee')), 'chan_id_in')
- forwards_df_out_7d_sum = DataFrame.from_records(forwards.values('chan_id_out').annotate(amt_out_msat=Sum('amt_out_msat'), fee=Sum('fee')), 'chan_id_out')
+ forwards_1d = forwards.filter(forward_date__gte=filter_1day)
+ if forwards_1d.exists():
+ forwards_df_in_1d_sum = DataFrame.from_records(forwards_1d.values('chan_id_in').annotate(amt_out_msat=Sum('amt_out_msat'), fee=Sum('fee')), 'chan_id_in')
+ if forwards.exists():
+ forwards_df_in_7d_sum = DataFrame.from_records(forwards.values('chan_id_in').annotate(amt_out_msat=Sum('amt_out_msat'), fee=Sum('fee')), 'chan_id_in')
+ forwards_df_out_7d_sum = DataFrame.from_records(forwards.values('chan_id_out').annotate(amt_out_msat=Sum('amt_out_msat'), fee=Sum('fee')), 'chan_id_out')
+ else:
+ forwards_df_in_7d_sum = DataFrame()
+ forwards_df_out_7d_sum = DataFrame()
else:
+ forwards_df_in_1d_sum = DataFrame()
forwards_df_in_7d_sum = DataFrame()
forwards_df_out_7d_sum = DataFrame()
+ channels_df['amt_routed_in_1day'] = channels_df.apply(lambda row: int(forwards_df_in_1d_sum.loc[row.chan_id].amt_out_msat/1000) if (forwards_df_in_1d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_routed_in_7day'] = channels_df.apply(lambda row: int(forwards_df_in_7d_sum.loc[row.chan_id].amt_out_msat/1000) if (forwards_df_in_7d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['amt_routed_out_7day'] = channels_df.apply(lambda row: int(forwards_df_out_7d_sum.loc[row.chan_id].amt_out_msat/1000) if (forwards_df_out_7d_sum.index == row.chan_id).any() else 0, axis=1)
channels_df['net_routed_7day'] = channels_df.apply(lambda row: round((row['amt_routed_out_7day']-row['amt_routed_in_7day'])/row['capacity'], 1), axis=1)
@@ -79,13 +87,13 @@ def main(channels):
failed_htlc_df = failed_htlc_df[(failed_htlc_df['wire_failure']==15) & (failed_htlc_df['failure_detail']==6) & (failed_htlc_df['amount']>failed_htlc_df['chan_out_liq']+failed_htlc_df['chan_out_pending'])]
lowliq_df['failed_out_1day'] = 0 if failed_htlc_df.empty else lowliq_df.apply(lambda row: len(failed_htlc_df[failed_htlc_df['chan_id_out']==row.chan_id]), axis=1)
# INCREASE IF (failed htlc > threshhold) && (flow in == 0)
- lowliq_df['new_rate'] = lowliq_df.apply(lambda row: row['local_fee_rate']+(5*multiplier) if row['failed_out_1day']>failed_htlc_limit and row['amt_routed_in_7day'] == 0 else row['local_fee_rate'], axis=1)
+ lowliq_df['new_rate'] = lowliq_df.apply(lambda row: row['local_fee_rate']+(5*multiplier) if row['failed_out_1day']>failed_htlc_limit and row['amt_routed_in_1day'] == 0 else row['local_fee_rate'], axis=1)
# Balanced Liquidity
balanced_df = channels_df[(channels_df['out_percent'] > lowliq_limit) & (channels_df['out_percent'] < excess_limit)].copy()
# IF NO FLOW THEN DECREASE FEE AND IF HIGH FLOW THEN SLOWLY INCREASE FEE
- balanced_df['new_rate'] = balanced_df.apply(lambda row: row['local_fee_rate']+((2*multiplier)*(1+(row['net_routed_7day']/row['capacity']))) if row['net_routed_7day'] > row['capacity'] else row['local_fee_rate'], axis=1)
- balanced_df['new_rate'] = balanced_df.apply(lambda row: row['local_fee_rate']-(3*multiplier) if (row['amt_routed_in_7day']+row['amt_routed_out_7day']) == 0 else row['local_fee_rate'], axis=1)
+ balanced_df['new_rate'] = balanced_df.apply(lambda row: row['local_fee_rate']+((2*multiplier)*(1+(row['net_routed_7day']/row['capacity']))) if row['net_routed_7day'] > 1 else row['local_fee_rate'], axis=1)
+ balanced_df['new_rate'] = balanced_df.apply(lambda row: row['local_fee_rate']-(3*multiplier) if (row['amt_routed_in_7day']+row['amt_routed_out_7day']) == 0 else row['new_rate'], axis=1)
# Excess Liquidity
excess_df = channels_df[channels_df['out_percent'] >= excess_limit].copy()
@@ -93,7 +101,7 @@ def main(channels):
excess_df['revenue_assist_7day'] = excess_df.apply(lambda row: int(forwards_df_in_7d_sum.loc[row.chan_id].fee) if forwards_df_in_7d_sum.empty == False and (forwards_df_in_7d_sum.index == row.chan_id).any() else 0, axis=1)
# DECREASE IF (assisting channel or stagnant liq)
excess_df['new_rate'] = excess_df.apply(lambda row: row['local_fee_rate']-(5*multiplier) if row['net_routed_7day'] < 0 and row['revenue_assist_7day'] > (row['revenue_7day']*10) else row['local_fee_rate'], axis=1)
- excess_df['new_rate'] = excess_df.apply(lambda row: row['local_fee_rate']-(5*multiplier) if (row['amt_routed_in_7day']+row['amt_routed_out_7day']) == 0 else row['local_fee_rate'], axis=1)
+ excess_df['new_rate'] = excess_df.apply(lambda row: row['local_fee_rate']-(5*multiplier) if (row['amt_routed_in_7day']+row['amt_routed_out_7day']) == 0 else row['new_rate'], axis=1)
#Merge back results
result_df = concat([lowliq_df, balanced_df, excess_df])
diff --git a/controller.py b/controller.py
new file mode 100644
index 00000000..913b1cbe
--- /dev/null
+++ b/controller.py
@@ -0,0 +1,23 @@
+import multiprocessing
+import jobs, rebalancer, htlc_stream, p2p
+
+def run_task(task):
+ task()
+
+def main():
+ tasks = [jobs.main, rebalancer.main, htlc_stream.main, p2p.main]
+ print('Controller is starting...')
+
+ processes = []
+ for task in tasks:
+ process = multiprocessing.Process(target=run_task, args=(task,))
+ processes.append(process)
+ process.start()
+
+ for process in processes:
+ process.join()
+
+ print('Controller is stopping...')
+
+if __name__ == '__main__':
+ main()
diff --git a/gui/forms.py b/gui/forms.py
index 9e9d3c98..d3ac9aee 100644
--- a/gui/forms.py
+++ b/gui/forms.py
@@ -1,32 +1,6 @@
from django import forms
from .models import Channels
-class RebalancerModelChoiceIterator(forms.models.ModelChoiceIterator):
- def choice(self, obj):
- return (self.field.prepare_value(obj),
- (str(obj.chan_id) + ' | ' + obj.alias + ' | ' + "{:,}".format(obj.local_balance) + ' | ' + obj.remote_pubkey))
-
-class RebalancerModelChoiceField(forms.models.ModelMultipleChoiceField):
- def _get_choices(self):
- if hasattr(self, '_choices'):
- return self._choices
- return RebalancerModelChoiceIterator(self)
- choices = property(_get_choices,
- forms.MultipleChoiceField._set_choices)
-
-class ChanPolicyModelChoiceIterator(forms.models.ModelChoiceIterator):
- def choice(self, obj):
- return (self.field.prepare_value(obj),
- (str(obj.chan_id) + ' | ' + obj.alias + ' | ' + str(obj.local_base_fee) + ' | ' + str(obj.local_fee_rate) + ' | ' + str(obj.local_cltv)))
-
-class ChanPolicyModelChoiceField(forms.models.ModelMultipleChoiceField):
- def _get_choices(self):
- if hasattr(self, '_choices'):
- return self._choices
- return ChanPolicyModelChoiceIterator(self)
- choices = property(_get_choices,
- forms.MultipleChoiceField._set_choices)
-
class OpenChannelForm(forms.Form):
peer_pubkey = forms.CharField(label='peer_pubkey', max_length=66)
local_amt = forms.IntegerField(label='local_amt')
@@ -34,7 +8,7 @@ class OpenChannelForm(forms.Form):
class CloseChannelForm(forms.Form):
chan_id = forms.CharField(label='chan_id')
- target_fee = forms.IntegerField(label='target_fee')
+ target_fee = forms.IntegerField(label='target_fee', required=False)
force = forms.BooleanField(widget=forms.CheckboxSelectMultiple, required=False)
class ConnectPeerForm(forms.Form):
@@ -59,7 +33,7 @@ class Meta:
fields = []
value = forms.IntegerField(label='value')
fee_limit = forms.IntegerField(label='fee_limit')
- outgoing_chan_ids = RebalancerModelChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Channels.objects.filter(is_open=1, is_active=1).order_by('-local_balance'), required=False)
+ outgoing_chan_ids = forms.ModelMultipleChoiceField(queryset=Channels.objects.filter(is_open=1, is_active=1), required=False)
last_hop_pubkey = forms.CharField(label='funding_txid', max_length=66, required=False)
duration = forms.IntegerField(label='duration')
diff --git a/gui/lnd_deps/lnd_connect.py b/gui/lnd_deps/lnd_connect.py
index ecef6f86..cac664d7 100644
--- a/gui/lnd_deps/lnd_connect.py
+++ b/gui/lnd_deps/lnd_connect.py
@@ -1,7 +1,7 @@
import os, codecs, grpc
from lndg import settings
-def creds():
+def get_creds():
#Open connection with lnd via grpc
with open(os.path.expanduser(settings.LND_MACAROON_PATH), 'rb') as f:
macaroon_bytes = f.read()
@@ -15,11 +15,12 @@ def metadata_callback(context, callback):
creds = grpc.composite_channel_credentials(cert_creds, auth_creds)
return creds
+creds = get_creds()
def lnd_connect():
- return grpc.secure_channel(settings.LND_RPC_SERVER, creds(), options=[('grpc.max_send_message_length', int(settings.LND_MAX_MESSAGE)*1000000), ('grpc.max_receive_message_length', int(settings.LND_MAX_MESSAGE)*1000000),])
+ return grpc.secure_channel(settings.LND_RPC_SERVER, creds, options=[('grpc.max_send_message_length', int(settings.LND_MAX_MESSAGE)*1000000), ('grpc.max_receive_message_length', int(settings.LND_MAX_MESSAGE)*1000000),])
def async_lnd_connect():
- return grpc.aio.secure_channel(settings.LND_RPC_SERVER, creds(), options=[('grpc.max_send_message_length', int(settings.LND_MAX_MESSAGE)*1000000), ('grpc.max_receive_message_length', int(settings.LND_MAX_MESSAGE)*1000000),])
+ return grpc.aio.secure_channel(settings.LND_RPC_SERVER, creds, options=[('grpc.max_send_message_length', int(settings.LND_MAX_MESSAGE)*1000000), ('grpc.max_receive_message_length', int(settings.LND_MAX_MESSAGE)*1000000),])
def main():
pass
diff --git a/gui/migrations/0037_tradesales.py b/gui/migrations/0037_tradesales.py
new file mode 100644
index 00000000..f39b1e52
--- /dev/null
+++ b/gui/migrations/0037_tradesales.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.6 on 2023-11-08 20:52
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gui', '0036_peers'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TradeSales',
+ fields=[
+ ('id', models.CharField(max_length=64, primary_key=True, serialize=False)),
+ ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+ ('expiry', models.DateTimeField(null=True)),
+ ('description', models.CharField(max_length=100)),
+ ('price', models.BigIntegerField()),
+ ('sale_type', models.IntegerField()),
+ ('secret', models.CharField(max_length=1000, null=True)),
+ ('sale_limit', models.IntegerField(null=True)),
+ ('sale_count', models.IntegerField(default=0)),
+ ],
+ ),
+ ]
diff --git a/gui/models.py b/gui/models.py
index 8d8ced6f..adcdb8f0 100644
--- a/gui/models.py
+++ b/gui/models.py
@@ -129,15 +129,15 @@ def save(self, *args, **kwargs):
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
+ LocalSettings(key='AR-Inbound%', value='90').save()
+ inbound_setting = 90
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)
else:
- LocalSettings(key='AR-Target%', value='5').save()
- amt_setting = 5
+ LocalSettings(key='AR-Target%', value='3').save()
+ amt_setting = 3
self.ar_amt_target = int((amt_setting/100) * self.capacity)
if not self.ar_max_cost:
if LocalSettings.objects.filter(key='AR-MaxCost%').exists():
@@ -325,4 +325,15 @@ class HistFailedHTLC(models.Model):
other_count = models.IntegerField()
class Meta:
app_label = 'gui'
- unique_together = (('date', 'chan_id_in', 'chan_id_out'),)
\ No newline at end of file
+ unique_together = (('date', 'chan_id_in', 'chan_id_out'),)
+
+class TradeSales(models.Model):
+ id = models.CharField(max_length=64, primary_key=True)
+ creation_date = models.DateTimeField(default=timezone.now)
+ expiry = models.DateTimeField(null=True)
+ description = models.CharField(max_length=100)
+ price = models.BigIntegerField()
+ sale_type = models.IntegerField()
+ secret = models.CharField(null=True, max_length=1000)
+ sale_limit = models.IntegerField(null=True)
+ sale_count = models.IntegerField(default=0)
\ No newline at end of file
diff --git a/gui/serializers.py b/gui/serializers.py
index e9bc48f3..041ce8bd 100644
--- a/gui/serializers.py
+++ b/gui/serializers.py
@@ -1,16 +1,18 @@
from rest_framework import serializers
from rest_framework.relations import PrimaryKeyRelatedField
-from .models import LocalSettings, Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, Peers, Onchain, PendingHTLCs, FailedHTLCs, Closures, Resolutions, PeerEvents
+from .models import LocalSettings, Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, Peers, Onchain, PendingHTLCs, FailedHTLCs, Closures, Resolutions, PeerEvents, TradeSales, Autofees
##FUTURE UPDATE 'exclude' TO 'fields'
class PaymentSerializer(serializers.HyperlinkedModelSerializer):
+ id = serializers.IntegerField(source='index')
payment_hash = serializers.ReadOnlyField()
class Meta:
model = Payments
exclude = []
class InvoiceSerializer(serializers.HyperlinkedModelSerializer):
+ id = serializers.IntegerField(source='index')
r_hash = serializers.ReadOnlyField()
creation_date = serializers.ReadOnlyField()
settle_date = serializers.ReadOnlyField()
@@ -100,6 +102,9 @@ class Meta:
class ConnectPeerSerializer(serializers.Serializer):
peer_id = serializers.CharField(label='peer_pubkey', max_length=200)
+class DisconnectPeerSerializer(serializers.Serializer):
+ peer_id = serializers.CharField(label='peer_pubkey', max_length=66)
+
class OpenChannelSerializer(serializers.Serializer):
peer_pubkey = serializers.CharField(label='peer_pubkey', max_length=66)
local_amt = serializers.IntegerField(label='local_amt')
@@ -119,6 +124,27 @@ class BumpFeeSerializer(serializers.Serializer):
class BroadcastTXSerializer(serializers.Serializer):
raw_tx = serializers.CharField(label='raw_tx')
+class CreateTradeSerializer(serializers.Serializer):
+ description = serializers.CharField(max_length=100)
+ price = serializers.IntegerField()
+ type = serializers.IntegerField()
+ secret = serializers.CharField(max_length=100, required=False, default=None)
+ expiry = serializers.DateTimeField(required=False, default=None)
+ sale_limit = serializers.IntegerField(required=False, default=None)
+
+class TradeSalesSerializer(serializers.HyperlinkedModelSerializer):
+ id = serializers.ReadOnlyField()
+ creation_date = serializers.ReadOnlyField()
+ sale_type = serializers.ReadOnlyField()
+ secret = serializers.ReadOnlyField()
+ sale_count = serializers.ReadOnlyField()
+ class Meta:
+ model = TradeSales
+ exclude = []
+
+class SignMessageSerializer(serializers.Serializer):
+ message = serializers.CharField(label='message')
+
class AddInvoiceSerializer(serializers.Serializer):
value = serializers.IntegerField(label='value')
@@ -190,8 +216,17 @@ def get_out_liq_percent(self, obj):
capacity = Channels.objects.filter(chan_id=obj.chan_id).get().capacity
return int(round((obj.out_liq/capacity)*100, 1))
+class FeeLogSerializer(serializers.HyperlinkedModelSerializer):
+ id = serializers.ReadOnlyField()
+ class Meta:
+ model = Autofees
+ exclude = []
+
class FailedHTLCSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = FailedHTLCs
- exclude = []
\ No newline at end of file
+ exclude = []
+
+class ResetSerializer(serializers.Serializer):
+ table = serializers.CharField(max_length=20)
\ No newline at end of file
diff --git a/gui/static/api.js b/gui/static/api.js
index afdad3ca..d8f9305c 100644
--- a/gui/static/api.js
+++ b/gui/static/api.js
@@ -23,7 +23,7 @@ async function DELETE(url, {method = 'DELETE'} = {}){
async function call({url, method, data, body, headers = {'Content-Type':'application/json'}}){
if(url.charAt(url.length-1) != '/') url += '/'
if(method != 'GET') headers['X-CSRFToken'] = document.getElementById('api').dataset.token
- const result = await fetch(`api/${url}${data ? '?': ''}${new URLSearchParams(data).toString()}`, {method, body: JSON.stringify(body), headers})
+ const result = await fetch(`/api/${url}${data ? '?': ''}${new URLSearchParams(data).toString()}`, {method, body: JSON.stringify(body), headers})
return result.json()
}
diff --git a/gui/static/helpers.js b/gui/static/helpers.js
index e3b16c90..7e7d5d30 100644
--- a/gui/static/helpers.js
+++ b/gui/static/helpers.js
@@ -2,7 +2,6 @@
function byId(id){ return document.getElementById(id) }
String.prototype.toInt = function(){ return parseInt(this.replace(/,/g,''))}
String.prototype.toBool = function(if_false = 0){ return this && /^true$/i.test(this) ? 1 : if_false}
-String.prototype.default = function(value){ return (this || '').length === 0 ? value : this}
Number.prototype.intcomma = function(){ return parseInt(this).toLocaleString() }
HTMLElement.prototype.defaultCloneNode = HTMLElement.prototype.cloneNode
HTMLElement.prototype.cloneNode = function(attrs){
@@ -32,7 +31,7 @@ function adjustTZ(datetime){
}
async function toggle(button){
try{
- button.children[0].style.visibility = 'collapse';
+ button.children[0].style.visibility = 'hidden';
button.children[1].style.visibility = 'visible';
navigator.clipboard.writeText(button.getAttribute('data-value'))
await sleep(1000)
@@ -42,7 +41,7 @@ async function toggle(button){
}
finally{
button.children[0].style.visibility = 'visible';
- button.children[1].style.visibility = 'collapse'
+ button.children[1].style.visibility = 'hidden'
}
}
function use(template){
@@ -105,43 +104,82 @@ function formatDate(start, end = new Date().getTime() + new Date().getTimezoneOf
end = new Date(end)
if (start == null) return '---'
difference = (end - new Date(start))/1000
- if (difference < 0) return 'Just now'
- if (difference < 60) {
+ if (difference > 0) {
+ if (difference < 60) {
if (Math.floor(difference) == 1){
return `a second ago`;
}else{
return `${Math.floor(difference)} seconds ago`;
}
- } else if (difference < 3600) {
- if (Math.floor(difference / 60) == 1){
- return `a minute ago`;
+ } else if (difference < 3600) {
+ if (Math.floor(difference / 60) == 1){
+ return `a minute ago`;
+ }else{
+ return `${Math.floor(difference / 60)} minutes ago`;
+ }
+ } else if (difference < 86400) {
+ if (Math.floor(difference / 3600) == 1){
+ return `an hour ago`;
+ }else{
+ return `${Math.floor(difference / 3600)} hours ago`;
+ }
+ } else if (difference < 2620800) {
+ if (Math.floor(difference / 86400) == 1){
+ return `a day ago`;
+ }else{
+ return `${Math.floor(difference / 86400)} days ago`;
+ }
+ } else if (difference < 31449600) {
+ if (Math.floor(difference / 2620800) == 1){
+ return `a month ago`;
+ }else{
+ return `${Math.floor(difference / 2620800)} months ago`;
+ }
+ } else {
+ if (Math.floor(difference / 31449600) == 1){
+ return `a year ago`;
+ }else{
+ return `${Math.floor(difference / 31449600)} years ago`;
+ }
+ }
+ } else if (difference < 0) {
+ if (-difference < 60) {
+ if (Math.floor(-difference) == 1){
+ return `in a second`;
}else{
- return `${Math.floor(difference / 60)} minutes ago`;
+ return `in ${Math.floor(-difference)} seconds`;
}
- } else if (difference < 86400) {
- if (Math.floor(difference / 3600) == 1){
- return `an hour ago`;
- }else{
- return `${Math.floor(difference / 3600)} hours ago`;
- }
- } else if (difference < 2620800) {
- if (Math.floor(difference / 86400) == 1){
- return `a day ago`;
- }else{
- return `${Math.floor(difference / 86400)} days ago`;
- }
- } else if (difference < 31449600) {
- if (Math.floor(difference / 2620800) == 1){
- return `a month ago`;
- }else{
- return `${Math.floor(difference / 2620800)} months ago`;
- }
- } else {
- if (Math.floor(difference / 31449600) == 1){
- return `a year ago`;
- }else{
- return `${Math.floor(difference / 31449600)} years ago`;
- }
- }
+ } else if (-difference < 3600) {
+ if (Math.floor(-difference / 60) == 1){
+ return `in a minute`;
+ }else{
+ return `in ${Math.floor(-difference / 60)} minutes`;
+ }
+ } else if (-difference < 86400) {
+ if (Math.floor(-difference / 3600) == 1){
+ return `in an hour`;
+ }else{
+ return `in ${Math.floor(-difference / 3600)} hours`;
+ }
+ } else if (-difference < 2620800) {
+ if (Math.floor(-difference / 86400) == 1){
+ return `in a day`;
+ }else{
+ return `in ${Math.floor(-difference / 86400)} days`;
+ }
+ } else if (-difference < 31449600) {
+ if (Math.floor(-difference / 2620800) == 1){
+ return `in a month`;
+ }else{
+ return `in ${Math.floor(-difference / 2620800)} months`;
+ }
+ } else {
+ if (Math.floor(-difference / 31449600) == 1){
+ return `in a year`;
+ }else{
+ return `in ${Math.floor(-difference / 31449600)} years`;
+ }
+ }
+ } else { return 'Just now' }
}
//END: COMMON FUNCTIONS & VARIABLES
\ No newline at end of file
diff --git a/gui/static/w3style.css b/gui/static/w3style.css
index dde29a9a..6a98ba3b 100644
--- a/gui/static/w3style.css
+++ b/gui/static/w3style.css
@@ -260,3 +260,13 @@ input:not([type=submit]):not([type=checkbox]):focus, select:focus {outline: none
.input {position:relative;float:left;padding:0px 4px}.input::after{position:absolute;right: 25px;top:2px;opacity:.8;content:attr(data-unit);color:#555599}
.encrypted td:not(th){-webkit-text-security:circle;}.encrypted thead th:not(a){-webkit-text-security:circle;}
.soft-encrypted table td, .soft-encrypted table thead{color: transparent;text-shadow: 0 0 5px rgba(0,0,0,0.1);}
+.switch{position:relative;display:inline-block;width:45px;height:25.5px}
+.switch input {opacity:0;width:0;height:0}
+.slider {position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;-webkit-transition:.4s;transition:.4s}
+.slider:before {position:absolute;content:"";height:19.5px;width:19.5px;left:3px;bottom:3px;background-color:white;-webkit-transition:.4s;transition:.4s}
+input:checked + .slider {background-color:#2196F3}
+input:focus + .slider {box-shadow:0 0 1px #2196F3}
+input:checked + .slider:before {-webkit-transform:translateX(19.5px);-ms-transform:translateX(19.5px);transform:translateX(19.5px)}
+.slider.round {border-radius:25.5px}
+.slider.round:before {border-radius:50%}
+pre {background-color: #f0f0f5;} .dark-mode pre{ background-color: #30363d; color:white }
\ No newline at end of file
diff --git a/gui/templates/advanced.html b/gui/templates/advanced.html
index 8ec53ee4..6f982834 100644
--- a/gui/templates/advanced.html
+++ b/gui/templates/advanced.html
@@ -19,7 +19,7 @@