Compare commits

..

No commits in common. 'master' and 'v0.10.2' have entirely different histories.

BIN
.DS_Store vendored

Binary file not shown.

@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

@ -0,0 +1,100 @@
version: 2
jobs:
# Define in CircleCi Project Variables: $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS
# Publish jobs require those variables
publish_docker_linuxamd64:
machine:
docker_layer_caching: false
steps:
- checkout
- run:
command: |
LATEST_TAG="${CIRCLE_TAG:1}"
DOCKERHUB_DESTINATION="$DOCKERHUB_REPO:$LATEST_TAG-amd64"
DOCKERHUB_DOCKERFILE="Dockerfile"
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker build --pull -t "$DOCKERHUB_DESTINATION" -f "$DOCKERHUB_DOCKERFILE" .
sudo docker push "$DOCKERHUB_DESTINATION"
publish_docker_linuxarm32v7:
machine:
docker_layer_caching: false
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG="${CIRCLE_TAG:1}"
DOCKERHUB_DESTINATION="$DOCKERHUB_REPO:$LATEST_TAG-arm32v7"
DOCKERHUB_DOCKERFILE="Dockerfile.arm32v7"
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker build --pull -t "$DOCKERHUB_DESTINATION" -f "$DOCKERHUB_DOCKERFILE" .
sudo docker push "$DOCKERHUB_DESTINATION"
publish_docker_linuxarm64v8:
machine:
docker_layer_caching: false
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG="${CIRCLE_TAG:1}"
DOCKERHUB_DESTINATION="$DOCKERHUB_REPO:$LATEST_TAG-arm64v8"
DOCKERHUB_DOCKERFILE="Dockerfile.arm64v8"
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker build --pull -t "$DOCKERHUB_DESTINATION" -f "$DOCKERHUB_DOCKERFILE" .
sudo docker push "$DOCKERHUB_DESTINATION"
publish_docker_multiarch:
machine:
enabled: true
image: circleci/classic:201808-01
steps:
- run:
command: |
# Turn on Experimental features
LATEST_TAG="${CIRCLE_TAG:1}"
sudo mkdir $HOME/.docker
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
sudo docker manifest create --amend "$DOCKERHUB_REPO:$LATEST_TAG" "$DOCKERHUB_REPO:$LATEST_TAG-amd64" "$DOCKERHUB_REPO:$LATEST_TAG-arm32v7" "$DOCKERHUB_REPO:$LATEST_TAG-arm64v8"
sudo docker manifest annotate "$DOCKERHUB_REPO:$LATEST_TAG" "$DOCKERHUB_REPO:$LATEST_TAG-amd64" --os linux --arch amd64
sudo docker manifest annotate "$DOCKERHUB_REPO:$LATEST_TAG" "$DOCKERHUB_REPO:$LATEST_TAG-arm32v7" --os linux --arch arm --variant v7
sudo docker manifest annotate "$DOCKERHUB_REPO:$LATEST_TAG" "$DOCKERHUB_REPO:$LATEST_TAG-arm64v8" --os linux --arch arm64 --variant v8
sudo docker manifest push "$DOCKERHUB_REPO:$LATEST_TAG" -p
workflows:
version: 2
publish:
jobs:
- publish_docker_linuxamd64:
filters:
branches:
ignore: /.*/
tags:
only: /v(?:(?<major>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<minor>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<patch>(?:0|[1-9](?:(?:0|[1-9])+)*))(?:-(?:([A-Za-z1-9])*))?)$/
- publish_docker_linuxarm32v7:
filters:
branches:
ignore: /.*/
tags:
only: /v(?:(?<major>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<minor>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<patch>(?:0|[1-9](?:(?:0|[1-9])+)*))(?:-(?:([A-Za-z1-9])*))?)$/
- publish_docker_linuxarm64v8:
filters:
branches:
ignore: /.*/
tags:
only: /v(?:(?<major>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<minor>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<patch>(?:0|[1-9](?:(?:0|[1-9])+)*))(?:-(?:([A-Za-z1-9])*))?)$/
- publish_docker_multiarch:
requires:
- publish_docker_linuxamd64
- publish_docker_linuxarm32v7
- publish_docker_linuxarm64v8
filters:
branches:
ignore: /.*/
tags:
only: /v(?:(?<major>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<minor>(?:0|[1-9](?:(?:0|[1-9])+)*))[.](?<patch>(?:0|[1-9](?:(?:0|[1-9])+)*))(?:-(?:([A-Za-z1-9])*))?)$/

@ -1,49 +0,0 @@
.angular/
.circleci/
.git/
.github/
.settings/
.vscode/
frontend/
backend/
backup/
cookies/
coverage/
dist/
docker/
dockerfiles/
logs/
node_modules/
node_modules_old/
node_modules_prod/
node_modules_dev/
out-tsc/
tmp/
typings/
.browserlistrc
_config.yml
.classpath
.DS_Store
.gitattributes
.gitignore
.idea
*.launch
.project
.sass-cache
*.sublime-workspace
.vscode/*
connect.lock
ECLDummyData.log
libpeerconnection.log
npm-debug.log
RTL-Config.json
RTL-Config-Old.json
RTL-Config-1.json
RTL-Multi-Node-Conf.json
RTL.conf
RTL-1.conf
RTL-Multi-Node-Conf-1.json
RTL-Config-for-BTC-Testing.json
testem.log
Thumbs.db
yarn-error.log

@ -1,215 +0,0 @@
{
"root": true,
"ignorePatterns": [
"backend/**/*.js"
],
"overrides": [
{
"files": [
"*.ts"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": "./tsconfig.json",
"createDefaultProgram": true
},
"env": { "es2022": true },
"plugins": ["deprecation"],
"extends": [
"plugin:@angular-eslint/all",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/component-selector": ["error", { "prefix": "rtl", "style": "kebab-case", "type": "element" }],
"@angular-eslint/directive-selector": ["error", { "style": "camelCase", "type": "attribute" }],
"@angular-eslint/consistent-component-styles": "off",
"@angular-eslint/prefer-on-push-component-change-detection": "off",
"@angular-eslint/prefer-standalone": "off",
"@angular-eslint/prefer-standalone-component": "off",
"@angular-eslint/sort-ngmodule-metadata-arrays": "off",
"@angular-eslint/use-component-view-encapsulation": "off",
"@angular-eslint/use-injectable-provided-in": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/type-annotation-spacing": 0,
"quotes": ["error", "single"],
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"computed-property-spacing": ["error", "never", { "enforceForClassMembers": true }],
"array-bracket-spacing": ["error", "never", { "objectsInArrays": false }],
"space-in-parens": ["error", "never"],
"eol-last": ["error", "always"],
"array-bracket-newline": ["error", "consistent"],
"curly": "error",
"no-unused-expressions": "error",
"strict": "error",
"max-len": ["error", { "code": 320 }],
"no-multiple-empty-lines": "error",
"no-trailing-spaces": "error",
"quote-props": ["error", "as-needed"],
"space-before-function-paren": ["error", { "anonymous": "never", "asyncArrow": "always", "named": "never" }],
"semi": ["error", "always"],
"dot-location": "error",
"no-await-in-loop": "error",
"no-console": "error",
"no-loss-of-precision": "error",
"no-promise-executor-return": "error",
"no-template-curly-in-string": "error",
"no-unreachable-loop": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"require-atomic-updates": "error",
"array-callback-return": "error",
"block-scoped-var": "error",
"default-case": "error",
"default-case-last": "error",
"eqeqeq": "error",
"grouped-accessor-pairs": "error",
"guard-for-in": "error",
"no-alert": "error",
"no-caller": "error",
"no-constructor-return": "error",
"no-div-regex": "error",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-floating-decimal": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-iterator": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-loop-func": "error",
"no-multi-str": "error",
"no-new": "error",
"no-new-func": "error",
"no-multi-spaces": "error",
"no-new-wrappers": "error",
"no-nonoctal-decimal-escape": "error",
"no-octal-escape": "error",
"no-proto": "error",
"no-restricted-properties": "error",
"no-return-assign": "error",
"no-return-await": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-unused-labels": "error",
"no-useless-call": "error",
"no-useless-concat": "error",
"no-useless-return": "error",
"no-void": "error",
"no-warning-comments": "error",
"prefer-named-capture-group": "error",
"prefer-promise-reject-errors": "error",
"prefer-regex-literals": "error",
"vars-on-top": "error",
"wrap-iife": "error",
"yoda": "error",
"no-label-var": "error",
"no-restricted-globals": "error",
"no-undef-init": "error",
"no-undefined": "error",
"block-spacing": "error",
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"comma-style": "error",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "error",
"id-match": "error",
"implicit-arrow-linebreak": "error",
"indent": ["error", 2, { "SwitchCase": 1, "MemberExpression": 1, "ArrayExpression": "off" }],
"keyword-spacing": ["error", { "before": true, "after": true, "overrides": { "this": { "before": false }}}],
"lines-around-comment": "error",
"max-depth": ["error", { "max": 7 }],
"max-nested-callbacks": "error",
"max-statements-per-line": ["error", { "max": 3 }],
"no-array-constructor": "error",
"no-continue": "error",
"no-mixed-operators": "error",
"no-multi-assign": "error",
"no-new-object": "error",
"no-restricted-syntax": "error",
"no-tabs": "error",
"no-unneeded-ternary": "error",
"no-whitespace-before-property": "error",
"nonblock-statement-body-position": "error",
"object-curly-newline": "error",
"object-curly-spacing": ["error", "always"],
"one-var-declaration-per-line": "error",
"operator-linebreak": ["error", "after"],
"padded-blocks": ["error", { "classes": "always", "blocks": "never", "switches": "never" }],
"padding-line-between-statements": "error",
"prefer-exponentiation-operator": "error",
"semi-spacing": "error",
"semi-style": "error",
"space-before-blocks": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"switch-colon-spacing": "error",
"template-tag-spacing": "error",
"unicode-bom": "error",
"wrap-regex": "error",
"arrow-body-style": "error",
"arrow-parens": "error",
"arrow-spacing": "error",
"generator-star-spacing": "error",
"no-confusing-arrow": "error",
"no-duplicate-imports": "error",
"no-restricted-exports": "error",
"no-restricted-imports": "error",
"no-useless-computed-key": "error",
"no-useless-rename": "error",
"no-var": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-numeric-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"rest-spread-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": "error",
"yield-star-spacing": "error"
}
},
{
"files": [
"*.html"
],
"parser": "@angular-eslint/template-parser",
"extends": [
"plugin:@angular-eslint/template/all",
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/arrow-body-style": "off",
"@angular-eslint/template/accessibility-elements-content": "off",
"@angular-eslint/template/accessibility-interactive-supports-focus": "off",
"@angular-eslint/template/button-has-type": "off",
"@angular-eslint/template/click-events-have-key-events": "off",
"@angular-eslint/template/conditional-complexity": "off",
"@angular-eslint/template/cyclomatic-complexity": "off",
"@angular-eslint/template/elements-content": "off",
"@angular-eslint/template/i18n": "off",
"@angular-eslint/template/no-autofocus": "off",
"@angular-eslint/template/no-call-expression": "off",
"@angular-eslint/template/no-inline-styles": "off",
"@angular-eslint/template/no-interpolation-in-attributes": "off",
"@angular-eslint/template/no-positive-tabindex": "off",
"@angular-eslint/template/prefer-ngsrc": "off",
"@angular-eslint/template/prefer-control-flow": "off",
"@angular-eslint/template/prefer-self-closing-tags": "off",
"@angular-eslint/template/use-track-by-function": "off"
}
}
]
}

@ -22,7 +22,7 @@ If applicable, add screenshots to help explain your problem.
**Your environment**
* Version of `RTL`
* Version of `lnd`/`core lightning`/`eclair`
* Version of `lnd`
* Version of `btcd`, `bitcoind`, or other backend
* Browser & browser version
* Operating system (`uname -a` on *Nix)

@ -1,76 +0,0 @@
RTL allows the user to configure and control specific application parameters for app customization and integration.<br />
The parameters can be configured via RTL-Config.json file or through environment variables defined at the OS level. Required <br />
parameters have `default` values for initial setup and can be updated after RTL server initial start.<br />
<br />
### RTL-Config.json<br />
```
{
"multiPass": "<The password in plain text, default 'password', Required>",
"port": "<port number for the rtl node server, default '3000', Required>",
"host": "<host for the rtl node server, default 'all IPs', Optional>",
"defaultNodeIndex": <Default index to load when rtl server starts, default 1, Optional>,
"dbDirectoryPath": "<Complete path of the folder where rtl database file should be saved, defults to RTL root, Optional>",
"SSO": {
"rtlSSO": <parameter to turn SSO off/on. Allowed values - 1 (single sign on via an external cookie), 0 (stand alone RTL authentication), Required>,
"rtlCookiePath": "<Full path of the cookie file including the file name. The application url needs to pass the value from this cookie file as query param 'access-key' for the SSO authentication to work, Required if SSO=1 else empty (Optional)>",
"logoutRedirectLink": "<URL to re-direct to after logout/timeout from RTL, Required if SSO=1 else empty (Optional)>"
},
"nodes": [
{
"index": <Incremental node indices starting from 1, Required>,
"lnNode": "<Node name to uniquely identify the node in the UI, Required>",
"lnImplementation": "<LNP implementation, Allowed values LND/CLN/ECL, Required>",
"authentication": {
"macaroonPath": "<Path for the folder containing 'admin.macaroon' for LND node, Required for LND>",
"runePath": "<Complete path including filename for CLN rune for the node, Required for CLN>",
"lnApiPassword": "<Password to be used for ECL API authentication. Mandatory only for ECL if the configPath is missing>"
"swapMacaroonPath": "<Path for the folder containing 'loop.macaroon' (LND), Required for LND Loop>",
"boltzMacaroonPath": "<Path for the folder containing 'admin.macaroon' (Boltz), Required for Boltz Swaps>",
"configPath": "<Full path of the lnd.conf/core lightning config/eclair.conf file including the file name, if present locally, Optional, only mandatory for ECL if the lnApiPassword is missing>",
},
"settings": {
"userPersona": "<User persona to tailor the data on UI. Allowed values MERCHANT/OPERATOR. Default MERCHANT, Optional>",
"themeMode": "<Theme modes, Allowed values DAY, NIGHT. Default DAY, Optional>",
"themeColor": "<Theme colors, Allowed values PURPLE, TEAL, INDIGO, PINK, YELLOW. Default PURPLE, Optional>",
"channelBackupPath": "<Path to save channel backup file. Only for LND implementation, Default <RTL root>\backup\node-1, Optional>",
"bitcoindConfigPath": "<Path of bitcoind.conf path if available locally>",
"logLevel": <logging levels, will log in accordance with the logLevel value provided, Allowed values ERROR, WARN, INFO, DEBUG>,
"fiatConversion": <parameter to turn fiat conversion off/on. Allowed values - true, false, default false, Optional>,
"currencyUnit": "<Optional: Fiat current Unit for currency conversion, default 'USD', Optional>",
"unannouncedChannels": <parameter to turn off/on setting for opening announced Channels, default false, Optional>
"lnServerUrl": "<Service url for LND/Core Lightning REST APIs for the node, e.g. https://192.168.0.1:8080 OR https://192.168.0.1:3001 OR http://192.168.0.1:8080. Default 'https://127.0.0.1:8080', Optional>
"swapServerUrl": "<Service url for swap server REST APIs for the node, e.g. https://127.0.0.1:8081, Optional>",
"boltzServerUrl": "<Service url for boltz server REST APIs for the node, e.g. https://127.0.0.1:9003, Optional>",
"blockExplorerUrl": "<url for local or centralized block explorer. e.g. https://mempool.space>"
}
}
]
}
```
<br />
### Environment variables<br />
The environment variable can also be used for all of the above configurations except the UI settings.<br />
If the environment variables are set, it will take precedence over the parameters in the RTL-Config.json file.<br />
<br />
PORT (port number for the rtl node server, default 3000, Optional)<br />
HOST (host for the rtl node server, default localhost, Optional)<br />
DB_DIRECTORY_PATH (Path for the folder where rtl database file should be saved, default RTL root directory, Optional)
APP_PASSWORD (Plaintext password to be provided by the parent container, NOT suggested for standalone RTL applications, to be used by Umbrel) (Optional)<br />
LN_IMPLEMENTATION (LND/CLN/ECL. Default 'LND', Optional)<br />
LN_SERVER_URL (LN server URL for LNP REST APIs, default https://127.0.0.1:8080) (Optional)<br />
SWAP_SERVER_URL (Swap server URL for REST APIs, default http://127.0.0.1:8081) (Optional)<br />
BOLTZ_SERVER_URL (Boltz server URL for REST APIs, default http://127.0.0.1:9003) (Optional)<br />
CONFIG_PATH (Full path of the LNP .conf file including the file name) (Optional for LND & CLN, Mandatory for ECL if LN_API_PASSWORD is undefined)<br />
MACAROON_PATH (Path for the folder containing 'admin.macaroon' for LND, Required for LND)<br />
RUNE_PATH (Complete path for the file containing 'rune' for CLN where the file should define the rune in 'LIGHTNING_RUNE="your-rune"' format, Required for CLN)<br />
SWAP_MACAROON_PATH (Path for the folder containing Loop's 'loop.macaroon', optional)<br />
BOLTZ_MACAROON_PATH (Path for the folder containing Boltz's 'admin.macaroon', optional)<br />
RTL_SSO (1 - single sign on via an external cookie, 0 - stand alone RTL authentication, Required)<br />
RTL_COOKIE_PATH (Full path of the cookie file including the file name, Required if RTL_SSO=1 else Optional)<br />
LOGOUT_REDIRECT_LINK (URL to re-direct to after logout/timeout from RTL, Required if RTL_SSO=1 else Optional)<br />
RTL_CONFIG_PATH (Path for the folder containing 'RTL-Config.json' file, Required)<br />
BITCOIND_CONFIG_PATH (Full path of the bitcoind.conf file including the file name, Optional)<br />
CHANNEL_BACKUP_PATH (Folder location for saving the channel backup files, valid for LND implementation only, Required if ln implementation=LND else Optional)<br />
ENABLE_OFFERS (Boolean flag to enable the offers feature on core lighning, default false, optional)<br />
ENABLE_PEERSWAP (Boolean flag to enable the peerswap feature on core lighning, default false, optional)<br />
LN_API_PASSWORD (Password for Eclair implementation if the eclair.conf path is not available, Required if ln implementation=ECL && config path is undefined)<br />

@ -1,111 +0,0 @@
![](../screenshots/RTL-CLN-Arch-3.png)
## RTL Core lightning setup
* [Introduction](#intro)
* [Pre-requisite](#prereq)
* [Architecture](#arch)
* [Installation](#install)
* [Prep for execution](#prep)
* [Start the server and access the app](#start)
### <a name="intro"></a>Introduction
RTL is now enabled to manage lightning nodes running Core Lightning
Follow the below steps to install and setup RTL to run on Core Lightning
### <a name="prereq"></a>Pre-requisites:
1. Functioning Core Lightning node. Follow install instructions on their [github](https://github.com/ElementsProject/lightning)
2. NodeJS - Can be downloaded [here](https://nodejs.org/en/download)
3. CLNRest - Ensure that core lightning's `CLNRest` API server is configured. Configuration instructions [here](https://docs.corelightning.org/docs/rest#configuration)
4. Create/reuse core-lightning's rune. Check [`createrune`](https://docs.corelightning.org/reference/lightning-createrune) and [`showrunes`](https://docs.corelightning.org/reference/lightning-showrunes) documentation for more details on how to create runes
4. Copy the `rune` and save it in a file which must be accessible to RTL. The content of the file must be `LIGHTNING_RUNE="<your-rune>"`
### <a name="arch"></a>Architecture
![](../screenshots/RTL-CLN-Arch-2.png)
### <a name="install"></a>Installation:
To download a specific RTL version follow the instructions on the [release page](https://github.com/Ride-The-Lightning/RTL/releases)
To download from master (*not recommended*):
#### First time setup
```
$ git clone https://github.com/Ride-The-Lightning/RTL.git
$ cd RTL
$ npm install --omit=dev --legacy-peer-deps
```
#### Or: Update existing build
```
$ cd RTL
$ git reset --hard HEAD
$ git clean -f -d
$ git pull
$ npm install --omit=dev --legacy-peer-deps
```
#### Error on npm install
If there is an error with `upstream dependency conflict` message then replace `npm install --omit=dev` with `npm install --omit=dev --legacy-peer-deps`.
### <a name="prep"></a>Prep for Execution
RTL requires its own config file `RTL-Config.json`, to start the server and provide user authentication on the app
* Rename the file `Sample-RTL-Config.json` to `RTL-Config.json` located at`./RTL`
* Locate the complete path of the readable `.commando` file on your node
* Modify the RTL conf file per the example file below
Ensure that the follow values are correct per your config:
* `lnImplementation` - This should be `CLN`, indicating that RTL is connecting to a core lightning node
* `runePath` - Path of the folder including **filename** which contains the `rune` for the node. The content of the file must be `LIGHTNING_RUNE="<your-rune>"`
* `lnServerUrl` - complete url with ip address and port of the CLNRest server
* `multiPass` - Specify the password (in plain text) to access RTL. This password will be hashed and not stored as plain text
* `configPath` (optional) - File path of the core lightning config file, if RTL server is local to the core lightning server
```
{
"multiPass": <password required for accessing RTL>,
"port": "3000",
"defaultNodeIndex": 1,
"dbDirectoryPath": "<Complete path of the folder where rtl's database file should be saved>",
"SSO": {
"rtlSSO": 0,
"rtlCookiePath": "",
"logoutRedirectLink": ""
},
"nodes": [
{
"index": 1,
"lnNode": "Core Lightning Testnet # 1",
"lnImplementation": "CLN",
"authentication": {
"runePath": "<Modify to include the path of the folder including filename which contains `rune`>",
"configPath": "<Optional - Config file path for core lightning>"
},
"settings": {
"userPersona": "OPERATOR",
"themeMode": "DAY",
"themeColor": "PURPLE",
"bitcoindConfigPath": "",
"logLevel": "INFO",
"fiatConversion": false,
"unannouncedChannels": false,
"lnServerUrl": "https://<CLNRest api server ip address>:3001",
"blockExplorerUrl": "<Default: https://mempool.space>"
}
}
]
}
```
### <a name="start"></a>Start the server and access the app
Run the following command:
`$ node rtl`
If the server started successfully, you should get the below output on the console:
`$ Server is up and running, please open the UI at http://localhost:3000 or your proxy configured url`
Open your browser at the following address: http://localhost:3000 to access the RTL app
### Detailed config and instructions
For detailed config and access options and other information, view the main readme page

@ -1,82 +0,0 @@
### Setup https access for RTL
Forward the ports 80 and 3002 on the router to the device running RTL.
Allow the ports through the firewall of the device.
Install, if needed, openssl
On Debian based distros:
$> sudo apt install openssl
Create a self certificate with openssl
$> openssl req -newkey rsa:4096 -x509 -sha512 -days 365 -nodes -out /path/to/some/folder/rtl-cert.crt -keyout /path/to/some/folder/rtl-cert.key
#### Nginx
Install Nginx:
https://www.nginx.com/resources/wiki/start/topics/tutorials/install/
On Debian based distros:
$> sudo apt install nginx
nginx default config file is at /etc/nginx/nginx.conf. You will need it.
Sample configuration to be inserted in the nginx.conf (adjust the path and filename of your certificate and key):
stream {
upstream RTL {
server 127.0.0.1:3000;
}
server {
listen 3002 ssl;
proxy_pass RTL;
ssl_certificate /path/to/some/folder/rtl-cert.crt;
ssl_certificate_key /path/to/some/folder/rtl-cert.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 4h;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # this line works for me with only TLSv1.2
ssl_prefer_server_ciphers on;
}
}
Restart Nginx with the new configuration and connect to RTL over https on the port 3002.
On Debian based distros:
$> sudo systemctl restart nginx
#### Apache2
Skip to step 5 if you already have Apache2 set up with HTTPS configured.
1. Install [Apache2](https://httpd.apache.org/download.cgi)
- On Debian-based distros: `sudo apt install apache2`
- On Fedora-based distros: `sudo yum install httpd`
2. Install [Let's Encrypt](https://letsencrypt.org) to get a free TLS certificate. The easiest way to install is to use Snap: `sudo snap install certbot`.
3. Run `certbot` to get a Let's Encrypt certificate: `sudo certbot`. Follow the instructions given to validate your domain name and install the certificate only. You may choose to redirect HTTP traffic to HTTPS instead (which attempts to secure every connection even when the client device does not request it). Let's Encrypt does not issue certificates for IP addresses. if you don't have a domain name, but you can use a service like NoIP.
4. Locate the Let's Encrypt Apache2 configuration file. It's usually in `/etc/apache2/sites-enabled` and called "000-default-le-ssl.conf" or similar.
5. Add the following lines to the Apache2 configuration file between the VIrtualHost 443 tags. This will redirect Apache's document root on your webserver to instead point to RTL. Change "/" to something like "/rtl" if you would instead like to redirect "/rtl" to RTL and do something else at the document root. Change "3002" to whatever port number you are using if it is not 3002.
ProxyPass "/" "http://127.0.0.1:3002/rtl"
ProxyPassReverse "/" "http://127.0.0.1:3002/rtl"
6. Restart Apache2.
- Debian-based distros: `sudo systemctl restart apache2`
- Fedora-based distros: `sudo systemctl restart httpd`
7. (Option) Edit ~/.rtl/rtl.js to disable insercure HTTP access to RTL entirely. Find these lines:
if (common.host) {
server.listen(common.port, common.host);
} else {
server.listen(common.port);
}
...and change them to:
if (common.host) {
server.listen(common.port, "127.0.0.1");
} else {
server.listen(common.port, "127.0.0.1");
}
This disables normal HTTP access to your server except if the client is on the same machine as the server. This will allow you to access http://localhost:3002 (or whatever port number you are using) on a browser that is on the same machine as RTL, but otherwise, you will have to access RTL through the Apache2 reverse proxy at https://yourdomain.tld/rtl, which will secure the connection with HTTPS.
Note: Occasionally you will receive "Invalid CSRF token, form tempered" when attempting to log in to RTL. If that happens, refresh the page and try again.

@ -1,47 +0,0 @@
### Connect to RTL remotely over Tor
This guide will allow you to remotely connect to RTL over Tor. This can work on any platform, the below example is for serving an android and windows client.
#### Server Setup
Install Tor on the same local machine as RTL. see the tor project wiki [here](https://trac.torproject.org/projects/tor/wiki)
On Debian based distros:
$> sudo apt install tor
Edit `/etc/tor/torrc` (Debian based distro) configuration file, and add the following lines:
```
HiddenServiceDir /var/lib/tor/rtl-service-v3/
HiddenServiceVersion 3
HiddenServicePort 3000 127.0.0.1:3000
```
Change `/var/lib/tor/rtl-service-v3/` to any directory you want to store the hidden service credentials.
Save the changes to the `torrc` file and restart tor.
$> sudo systemctl restart tor
or sometimes:
$> sudo systemctl daemon-reload
View the contents of the file `/var/lib/tor/rtl-service-v3/hostname`. You need to be root. It will show an onion address. This is your address.
On Debian based distro:
$> su -c "cat /var/lib/tor/rtl-service-v3/hostname"
#### Client setup: Android
Install Tor browser (or any other compatible browser) for Android from the app store
Open the tor enabled browser and type in the onion address (example `z1234567890abc.onion:3000`)
Only you have access to this website! All traffic in the tor enabled browser will go over Tor (which is slower than clearnet).
#### Client setup: Windows Tor Browser (not updated)
Download and install Tor Browser for windows: https://www.torproject.org/download/
In Windows, edit `"%HOMEDRIVE%%HOMEPATH%"\Desktop\Tor Browser\Browser\TorBrowser\Data\Tor\torrc`
Add the following line. Replace the onion address, password(cookie), and mydevice with your credentials:
```
HidServAuth 1234567890abcdefg.onion abcdef01234567890+/K mydevice
```
Save and exit.
Now open Tor Browser, type in the `1234567890abcdefg.onion:3000` address!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

@ -1,92 +0,0 @@
name: Lint & Test
on:
push:
tags: [ 'v*' ]
release:
types: [released]
# Triggers the workflow only when merging pull request to the branches.
pull_request:
types: [closed]
branches: [ master, 'Release-*' ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 18.x
- name: Cache node_modules
uses: actions/cache@v2
id: cache-npm-packages
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
- name: Install NPM dependencies
if: steps.cache-npm-packages.outputs.cache-hit != 'true'
run: npm ci --legacy-peer-deps
lint:
name: Lint
runs-on: ubuntu-latest
needs: prepare
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 18.x
- name: Cache node_modules
uses: actions/cache@v2
id: cache-npm-packages
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
- name: Install NPM dependencies
if: steps.cache-npm-packages.outputs.cache-hit != 'true'
run: npm ci --legacy-peer-deps
- name: Lint Src and Server
run: npm run lint
test:
name: Test
runs-on: ubuntu-latest
needs: prepare
env:
CI: true
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 18.x
- name: Cache node_modules
uses: actions/cache@v2
id: cache-npm-packages
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
- name: Install NPM dependencies
if: steps.cache-npm-packages.outputs.cache-hit != 'true'
run: npm ci --legacy-peer-deps
- name: Run tests
run: npm run test

@ -1,86 +0,0 @@
name: Artifact
on:
push:
branches: [ master, 'Release-*' ]
tags: [ 'v*' ]
release:
types: [released]
# Triggers the workflow only when merging pull request to the branches.
pull_request:
types: [closed]
branches: [ master, 'Release-*' ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 18.x
- name: Cache node_modules
uses: actions/cache@v2
id: cache-npm-packages
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
- name: Install NPM dependencies
if: steps.cache-npm-packages.outputs.cache-hit != 'true'
run: npm ci --legacy-peer-deps
- name: Cache build frontend
uses: actions/cache@v2
id: cache-build-frontend
with:
path: frontend
key: ${{ runner.os }}-frontend-${{ github.sha }}
- name: Run build production application
run: npm run buildfrontend
- name: Cache build backend
uses: actions/cache@v2
id: cache-build-backend
with:
path: backend
key: ${{ runner.os }}-backend-${{ github.sha }}
- name: Run build backend server
run: npm run buildbackend
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Cache build frontend
uses: actions/cache@v2
id: cache-build-frontend
with:
path: frontend
key: ${{ runner.os }}-frontend-${{ github.sha }}
- name: Cache build backend
uses: actions/cache@v2
id: cache-build-backend
with:
path: backend
key: ${{ runner.os }}-backend-${{ github.sha }}
- name: Compress files
run: tar -czf /tmp/rtlbuild.tar.gz frontend backend rtl.js package.json package-lock.json
- uses: actions/upload-artifact@v2
with:
name: rtl-build-${{ github.event.release.tag_name }}
path: /tmp/rtlbuild.tar.gz

@ -1,52 +0,0 @@
name: Build docker images
on:
push:
tags: [ 'v*' ]
workflow_dispatch:
inputs:
version:
description: 'Release version'
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up version
id: set-version
run: |
if [ "${{ github.event.inputs.version }}" != "" ]; then
VERSION=${{ github.event.inputs.version }}
elif [ "${{ github.ref_type }}" == "tag" ]; then
VERSION=${{ github.ref_name }}
else
echo "No version provided and no tag found."
exit 1
fi
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
shahanafarooqui/rtl:${{ env.VERSION }}

16
.gitignore vendored

@ -27,24 +27,19 @@
!.vscode/extensions.json
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
/db
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
channels-backup
logs
cookies
# System Files
.DS_Store
Thumbs.db
/database/*
/logs/*
/cookies/*
/backup/*
@ -58,12 +53,3 @@ RTL-1.conf
RTL-Multi-Node-Conf-1.json
RTL-Config-for-BTC-Testing.json
ECLDummyData.log
_config.yml
.vscode/launch.json
RTL-Config-Docker.json
dockerfiles/RTL-Config.json
dockerfiles/.env
RTL-Config-Regtest.json
RTL-Config-Signet.json
RTL-Config-Testnet.json
RTL-Config-All.json

@ -1,20 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"env": {
"NODE_ENV": "development"
},
"program": "${workspaceFolder}/rtl.js"
}
]
}

@ -1,13 +0,0 @@
{
"eslint.enable": true,
"eslint.validate": [
"typescript",
"HTML"
],
"eslint.options": {
"configFile": ".eslintrc.json"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

@ -1,36 +1,16 @@
ARG BASE_DISTRO="node:alpine"
FROM node:10-alpine
FROM --platform=${BUILDPLATFORM} ${BASE_DISTRO} as builder
RUN apk add --no-cache tini
WORKDIR /RTL
COPY package.json /RTL/package.json
COPY package-lock.json /RTL/package-lock.json
RUN npm install --legacy-peer-deps
COPY . .
# Build the Angular application
RUN npm run buildfrontend
# Build the Backend from typescript server
RUN npm run buildbackend
# Remove non production necessary modules
RUN npm prune --omit=dev --legacy-peer-deps
FROM --platform=$BUILDPLATFORM ${BASE_DISTRO} as runner
RUN apk add --no-cache tini
WORKDIR /RTL
# Install dependencies
RUN npm install --only=prod
COPY --from=builder /RTL/rtl.js ./rtl.js
COPY --from=builder /RTL/package.json ./package.json
COPY --from=builder /RTL/frontend ./frontend
COPY --from=builder /RTL/backend ./backend
COPY --from=builder /RTL/node_modules/ ./node_modules
COPY . /RTL
EXPOSE 3000

@ -0,0 +1,33 @@
FROM node:10-jessie-slim AS builder
ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini-static-armel /tini
ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini-static-armel.asc /tini.asc
RUN apt-get install gnupg
RUN gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \
&& gpg --batch --verify /tini.asc /tini
RUN chmod +x /tini
WORKDIR /RTL
COPY . /RTL
COPY package.json /RTL/package.json
COPY package-lock.json /RTL/package-lock.json
# Install dependencies
RUN npm install --only=prod
COPY . /RTL
FROM arm32v7/node:10-jessie-slim
WORKDIR /RTL
COPY --from=builder "/RTL" .
COPY --from=builder "/tini" /sbin/tini
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "-g", "--"]
CMD ["node", "rtl"]

@ -0,0 +1,29 @@
FROM node:10-stretch-slim AS builder
ADD https://github.com/krallin/tini/releases/download/v0.18.0/tini-static-arm64 /tini
RUN chmod +x /tini
WORKDIR /RTL
COPY . /RTL
COPY package.json /RTL/package.json
COPY package-lock.json /RTL/package-lock.json
# Install dependencies
RUN npm install --only=prod
COPY . /RTL
FROM arm64v8/node:10-stretch-slim
WORKDIR /RTL
COPY --from=builder "/RTL" .
COPY --from=builder "/tini" /sbin/tini
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "-g", "--"]
CMD ["node", "rtl"]

@ -1,10 +1,10 @@
## Ride The Lightning (RTL)
![](./screenshots/RTL-LND-Dashboard.png)
![](screenshots/RTL-LND-Dashboard.png)
<a href="https://snyk.io/test/github/Ride-The-Lightning/RTL"><img src="https://snyk.io/test/github/Ride-The-Lightning/RTL/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/Ride-The-Lightning/RTL" style="max-width:100%;"></a>
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE)
**Intro** -- [Application Features](./docs/Application_features.md) -- [Road Map](./docs/Roadmap.md) -- [Application Configurations](./docs/Application_configurations.md) -- [Core Lightning](./docs/Core_lightning_setup.md) -- [Eclair](./docs/Eclair_setup.md) -- [Contribution](./docs/Contributing.md)
**Intro** -- [Application Features](docs/Application_features.md) -- [Road Map](docs/Roadmap.md) -- [Application Configurations](docs/Application_configurations) -- [C-Lightning](docs/C-Lightning-setup.md) -- [Eclair](docs/Eclair-setup.md) -- [Contribution](docs/Contributing.md)
* [Introduction](#intro)
* [Architecture](#arch)
@ -17,12 +17,14 @@
### <a name="intro"></a>Introduction
RTL is a full function, device agnostic, web user interface to help manage lightning node operations.
RTL is available on [LND](https://github.com/lightningnetwork/lnd), [CoreLightning](https://github.com/ElementsProject/lightning) and [Eclair](https://github.com/ACINQ/eclair) implementations.
RTL is available on [LND](https://github.com/lightningnetwork/lnd), [C-Lightning](https://github.com/ElementsProject/lightning) and [Eclair](https://github.com/ACINQ/eclair) implementations.
* Core Lightning users, refer to [this](./docs/Core_lightning_setup.md) page for install instructions.
* Eclair users, refer to [this](./docs/Eclair_setup.md) page for install instructions.
* C-Lightning users, refer to [this](docs/C-Lightning-setup.md) page for install instructions.
* Eclair users, refer to [this](docs/Eclair-setup.md) page for install instructions.
* LND users, follow the instructions below
Lightning Network Daemon(LND) is an implementation of Lightning Network BOLT protocol by [Lightning Labs](https://lightning.engineering/).
Pre-requisite for running RTL is a functioning and synced LND node. If you are a Raspberry Pi or a Linux user, you can follow the famous Stadicus's [guide](https://stadicus.github.io/RaspiBolt/) to setup a Bitcoin + Lighting node.
RTL is available on the below platforms/services:
@ -35,12 +37,11 @@ RTL is available on the below platforms/services:
* [BCubium](https://bgeometrics.com)
* [Start9Labs](https://start9labs.com)
* [Umbrel](https://github.com/getumbrel/umbrel)
* [Sovran Systems](https://sovransystems.com)
Docker Image: https://hub.docker.com/r/shahanafarooqui/rtl
### <a name="arch"></a>Architecture
![](./screenshots/RTL-LND-Arch-2.png)
![](screenshots/RTL-LND-Arch-2.png)
### <a name="prereq"></a>Prerequisites
* Functioning and synced LND lightning node.
@ -55,7 +56,7 @@ To download from master (*not recommended*):
```
$ git clone https://github.com/Ride-The-Lightning/RTL.git
$ cd RTL
$ npm install --omit=dev --legacy-peer-deps
$ npm install --only=prod
```
#### Or: Update existing dependencies
```
@ -63,18 +64,14 @@ $ cd RTL
$ git reset --hard HEAD
$ git clean -f -d
$ git pull
$ npm install --omit=dev --legacy-peer-deps
$ npm install --only=prod
```
#### Error on npm install
If there is an error with `upstream dependency conflict` message then replace `npm install --omit=dev` with `npm install --omit=dev --legacy-peer-deps`.
### <a name="prep"></a>Prep for Execution
RTL requires its own config file `RTL-Config.json`, to start the server and provide user authentication on the app.
*Advanced users can refer to [this page](./docs/Multi_node_setup.md), for config settings required to manage multiple nodes*
*Advanced users can refer to [this page](docs/Multi-Node-setup.md), for config settings required to manage multiple nodes*
* Rename the file `Sample-RTL-Config.json` to `RTL-Config.json` located at`./RTL`.
* Rename `sample-RTL-Config.json` file to `RTL-Config.json`.
* Locate the complete path of the readable macroon file (admin.macroon) on your node and the lnd.conf file.
* Modify the `RTL-Config.json` file per the example file below
@ -84,7 +81,6 @@ Example RTL-Config.json:
"multiPass": "password",
"port": "3000",
"defaultNodeIndex": 1,
"dbDirectoryPath": "<Complete path of the folder where rtl's database file should be saved>",
"SSO": {
"rtlSSO": 0,
"rtlCookiePath": "",
@ -95,33 +91,30 @@ Example RTL-Config.json:
"index": 1,
"lnNode": "LND Testnet",
"lnImplementation": "LND",
"authentication": {
"macaroonPath": "<Complete path of the folder containing LND admin.macaroon for the node>",
"runePath": "<Complete path including filename for CLN rune for the node, rune format 'LIGHTNING_RUNE="your-rune"'>",
"lnApiPassword": "<Can be used to provide password in ECL implementation>",
"Authentication": {
"macaroonPath": "<Complete path of the folder containing LND's admin.macaroon for the node # 1>",
"swapMacaroonPath": "<Complete path of the folder containing Loop's loop.macaroon for the node>",
"boltzMacaroonPath": "<Complete path of the folder containing Boltz admin.macaroon for the node>",
"configPath": "<Optional:Path of the .conf if present locally or empty>",
"lnApiPassword": "<Optional:Can be used to provide password in ECL implementation>"
},
"settings": {
"Settings": {
"userPersona": "OPERATOR",
"themeMode": "DAY",
"themeColor": "PURPLE",
"channelBackupPath": "C:\\RTL\\backup\\node-1",
"bitcoindConfigPath": "<Optional: path of bitcoind.conf path if available locally>",
"logLevel": "INFO",
"enableLogging": true,
"fiatConversion": false,
"unannouncedChannels": false,
"lnServerUrl": "<url for LND REST APIs for node #1 e.g. https://192.168.0.1:8080>",
"swapServerUrl": "<url for swap server REST APIs for the node. e.g. https://127.0.0.1:8081>",
"boltzServerUrl": "<url for boltz server REST APIs for the node. e.g. https://127.0.0.1:9003>",
"blockExplorerUrl": "<url for local or centralized block explorer. e.g. https://mempool.space>"
"swapServerUrl": "<url for swap server REST APIs for the node. e.g. https://localhost:8081>",
"boltzServerUrl": "<url for boltz server REST APIs for the node. e.g. https://localhost:9003>"
}
}
]
}
```
For details on all the configuration options refer to [this page](./docs/Application_configurations.md).
For details on all the configuration options refer to [this page](./docs/Application_configurations).
#### User Authentication on RTL
RTL requires the user to be authenticated by the application first, before allowing access to LND functions.
@ -134,7 +127,7 @@ Run the following command:
If the server started successfully, you should get the below output on the console:
`$ Server is up and running, please open the UI at http://localhost:3000 or your proxy configured url.`
`$ Server is up and running, please open the UI at http://localhost:3000`
#### Optional: Running RTL as a service (Rpi or Linux platform users)
In case you are running a headless Rpi or a Linux node, you can configure RTL as a service.
@ -179,11 +172,11 @@ Open your browser at the following address: http://localhost:3000 to access the
* Determine the IP address of your node to access the application.
E.g. if the IP address of your node is 192.168.0.15 then open your browser at the following address: http://192.168.0.15:3000 to access RTL.
3. Config tweaks for running RTL server and LND on separate devices on the same network can be found [here](./docs/RTL_setups.md).
3. Config tweaks for running RTL server and LND on separate devices on the same network can be found [here](docs/RTL_setups.md).
4. Any Other setup: **Please be advised, if you are accessing your node remotely via RTL, its critical to encrypt the communication via use of https. You can use solutions like nginx and letsencrypt or TOR to setup secure access for RTL.**
- Sample SSL setup guide can be found [here](./docs/RTL_SSL_setup.md)
- (For advanced users) A sample SSL guide to serve remote access over an encrypted Tor connection can be found [here](./docs/RTL_TOR_setup.md)
- Sample SSL setup guide can be found [here](docs/RTL_SSL_setup.md)
- (For advanced users) A sample SSL guide to serve remote access over an encrypted Tor connection can be found [here](docs/RTL_TOR_setup.md)
### <a name="trouble"></a>Troubleshooting
In case you are running into issues with the application or if you have feedback, feel free to open issues on our github repo.

@ -0,0 +1 @@
theme: jekyll-theme-hacker

@ -8,6 +8,9 @@
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
@ -18,104 +21,116 @@
"builder": "@angular-devkit/build-angular:browser",
"options": {
"baseHref": "/rtl/",
"outputPath": "frontend",
"outputPath": "angular",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js",
"src/polyfills.ts"
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"allowedCommonJsDependencies": [
"hammerjs",
"sha256",
"qrcode",
"otplib"
],
"tsConfig": "src/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/assets"
],
"styles": [
"src/app/shared/theme/styles/styles.scss",
"node_modules/material-icons/iconfont/material-icons.css",
"node_modules/roboto-fontface/css/roboto/roboto-fontface.css"
"src/app/shared/theme/styles/styles.scss"
],
"scripts": [],
"allowedCommonJsDependencies": [
"buffer",
"rfdc",
"sha256",
"qrcode",
"otplib",
"pdfmake/build/pdfmake",
"pdfmake/build/vfs_fonts"
]
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "20mb",
"maximumError": "50mb"
"maximumWarning": "5mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "20mb",
"maximumError": "50mb"
"maximumWarning": "1mb",
"maximumError": "5mb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
]
}
},
"defaultConfiguration": "production"
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "RTLApp:build"
},
"configurations": {
"production": {
"buildTarget": "RTLApp:build:production"
},
"development": {
"buildTarget": "RTLApp:build:development"
"browserTarget": "RTLApp:build:production"
}
},
"defaultConfiguration": "development"
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "RTLApp:build"
"browserTarget": "RTLApp:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing",
"src/polyfills.ts"
],
"tsConfig": "src/tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/assets"
],
"styles": [
"src/app/shared/theme/styles/styles.scss",
"node_modules/material-icons/iconfont/material-icons.css",
"node_modules/roboto-fontface/css/roboto/roboto-fontface.css"
"src/app/shared/theme/styles/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "RTLApp:serve"
},
"configurations": {
"production": {
"devServerTarget": "RTLApp:serve:production"
}
}
}
}
}
},
"cli": {
"analytics": false
}
"defaultProject": "RTLApp"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@ -3,12 +3,12 @@
"short_name": "",
"icons": [
{
"src": "/rtl/assets/images/favicon-dark/android-chrome-192x192.png",
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/rtl/assets/images/favicon-dark/android-chrome-512x512.png",
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@ -3,12 +3,12 @@
"short_name": "",
"icons": [
{
"src": "/rtl/assets/images/favicon-light/android-chrome-192x192.png",
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/rtl/assets/images/favicon-light/android-chrome-512x512.png",
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RTL</title>
<base href="/rtl/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="assets/images/favicon-light/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon-light/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-light/favicon-16x16.png">
<link rel="manifest" href="assets/images/favicon-light/site.webmanifest">
<link rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="styles.34373e0f495fd5a53b6a.css"></head>
<body>
<rtl-app></rtl-app>
<script src="runtime.76e78b7c520ee74ce1af.js" defer></script><script src="polyfills.a290c5ced4c403cfee17.js" defer></script><script src="main.023be7f19d26fb1d812e.js" defer></script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise(function(r,n){t=o[e]=[r,n]});r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"237c2154eb3e4109a251",5:"25446aaee9f74a416633",6:"eee0e0f329ec52d3bce7",7:"e4068d65b9329bce7749"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout(function(){u({type:"timeout",target:i})},12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);

File diff suppressed because one or more lines are too long

121
app.js

@ -0,0 +1,121 @@
const path = require("path");
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const common = require("./common");
const app = express();
const baseHref = "/rtl/";
const apiRoot = baseHref + "api/";
const apiLNDRoot = baseHref + "api/lnd/";
const apiCLRoot = baseHref + "api/cl/";
const apiECLRoot = baseHref + "api/ecl/";
const authenticateRoutes = require("./routes/shared/authenticate");
const RTLConfRoutes = require("./routes/shared/RTLConf");
const loopRoutes = require('./routes/shared/loop');
const boltzRoutes = require('./routes/shared/boltz');
const infoRoutes = require("./routes/lnd/getInfo");
const channelsRoutes = require("./routes/lnd/channels");
const channelsBackupRoutes = require("./routes/lnd/channelsBackup");
const peersRoutes = require("./routes/lnd/peers");
const feesRoutes = require("./routes/lnd/fees");
const balanceRoutes = require("./routes/lnd/balance");
const walletRoutes = require("./routes/lnd/wallet");
const graphRoutes = require("./routes/lnd/graph");
const newAddressRoutes = require("./routes/lnd/newAddress");
const transactionsRoutes = require("./routes/lnd/transactions");
const payReqRoutes = require("./routes/lnd/payReq");
const paymentsRoutes = require("./routes/lnd/payments");
const invoiceRoutes = require("./routes/lnd/invoices");
const switchRoutes = require("./routes/lnd/switch");
const messageRoutes = require("./routes/lnd/message");
const infoCLRoutes = require("./routes/c-lightning/getInfo");
const feesCLRoutes = require("./routes/c-lightning/fees");
const balanceCLRoutes = require("./routes/c-lightning/balance");
const channelsCLRoutes = require("./routes/c-lightning/channels");
const invoicesCLRoutes = require("./routes/c-lightning/invoices");
const onChainCLRoutes = require("./routes/c-lightning/onchain");
const paymentsCLRoutes = require("./routes/c-lightning/payments");
const peersCLRoutes = require("./routes/c-lightning/peers");
const networkCLRoutes = require("./routes/c-lightning/network");
const messageCLRoutes = require("./routes/c-lightning/message");
const infoECLRoutes = require("./routes/eclair/getInfo");
const feesECLRoutes = require("./routes/eclair/fees");
const channelsECLRoutes = require("./routes/eclair/channels");
const onChainECLRoutes = require("./routes/eclair/onchain");
const peersECLRoutes = require("./routes/eclair/peers");
const invoicesECLRoutes = require("./routes/eclair/invoices");
const paymentsECLRoutes = require("./routes/eclair/payments");
const networkECLRoutes = require("./routes/eclair/network");
app.set('trust proxy', true);
app.use(cookieParser(common.secret_key));
app.use(bodyParser.json({limit: '25mb'}));
app.use(bodyParser.urlencoded({extended: false, limit: '25mb'}));
app.use(baseHref, express.static(path.join(__dirname, "angular")));
// CORS fix, Only required for developement due to separate backend and frontend servers
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization, filePath"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PATCH, PUT, DELETE, OPTIONS"
);
next();
});
// CORS fix, Only required for developement due to separate backend and frontend servers
app.use(apiRoot + "authenticate", authenticateRoutes);
app.use(apiRoot + "conf", RTLConfRoutes);
app.use(apiRoot + "boltz", boltzRoutes);
app.use(apiLNDRoot + "getinfo", infoRoutes);
app.use(apiLNDRoot + "channels", channelsRoutes);
app.use(apiLNDRoot + "channels/backup", channelsBackupRoutes);
app.use(apiLNDRoot + "peers", peersRoutes);
app.use(apiLNDRoot + "fees", feesRoutes);
app.use(apiLNDRoot + "balance", balanceRoutes);
app.use(apiLNDRoot + "wallet", walletRoutes);
app.use(apiLNDRoot + "network", graphRoutes);
app.use(apiLNDRoot + "newaddress", newAddressRoutes);
app.use(apiLNDRoot + "transactions", transactionsRoutes);
app.use(apiLNDRoot + "payreq", payReqRoutes);
app.use(apiLNDRoot + "payments", paymentsRoutes);
app.use(apiLNDRoot + "invoices", invoiceRoutes);
app.use(apiLNDRoot + "switch", switchRoutes);
app.use(apiLNDRoot + "loop", loopRoutes);
app.use(apiLNDRoot + "message", messageRoutes);
app.use(apiCLRoot + "getinfo", infoCLRoutes);
app.use(apiCLRoot + "fees", feesCLRoutes);
app.use(apiCLRoot + "balance", balanceCLRoutes);
app.use(apiCLRoot + "channels", channelsCLRoutes);
app.use(apiCLRoot + "invoices", invoicesCLRoutes);
app.use(apiCLRoot + "onchain", onChainCLRoutes);
app.use(apiCLRoot + "payments", paymentsCLRoutes);
app.use(apiCLRoot + "peers", peersCLRoutes);
app.use(apiCLRoot + "network", networkCLRoutes);
app.use(apiCLRoot + "message", messageCLRoutes);
app.use(apiECLRoot + "getinfo", infoECLRoutes);
app.use(apiECLRoot + "fees", feesECLRoutes);
app.use(apiECLRoot + "channels", channelsECLRoutes);
app.use(apiECLRoot + "onchain", onChainECLRoutes);
app.use(apiECLRoot + "peers", peersECLRoutes);
app.use(apiECLRoot + "invoices", invoicesECLRoutes);
app.use(apiECLRoot + "payments", paymentsECLRoutes);
app.use(apiECLRoot + "network", networkECLRoutes);
app.use((req, res, next) => {
res.sendFile(path.join(__dirname, "angular", "index.html"));
});
module.exports = app;

@ -1,116 +0,0 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { getAlias } from './network.js';
let options = null;
const logger = Logger;
const common = Common;
export const listPeerChannels = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Peer Channels..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listpeerchannels';
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Peer Channels List Received', data: body.channels });
return Promise.all(body.channels?.map((channel) => {
channel.to_them_msat = channel.total_msat - channel.to_us_msat;
channel.balancedness = (channel.total_msat === 0) ? 1 : (1 - Math.abs((channel.to_us_msat - (channel.total_msat - channel.to_us_msat)) / channel.total_msat)).toFixed(3);
return getAlias(req.session.selectedNode, channel, 'peer_id');
})).then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Peer Channels List With Aliases Received', data: body.channels });
return res.status(200).json(body.channels || []);
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'List Peer Channels Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const openChannel = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Opening Channel..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/fundchannel';
options.body = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Open Channel Options', data: options.body });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Opened', data: body });
res.status(201).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Open Channel Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const setChannelFee = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Setting Channel Fee..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/setchannel';
options.body = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Update Channel Policy Options', data: options.body });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Updated Channel Policy', data: body });
res.status(201).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Update Channel Policy Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const closeChannel = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' });
req.setTimeout(60000 * 10); // timeout 10 mins
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/close';
options.body = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Closing Channel', data: options.url });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Closed', data: body });
res.status(204).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Close Channel Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const listForwards = (req, res, next) => {
const { status } = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channel List Forwards..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listforwards';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Forwarding History Received For Status ' + status, data: body });
body.forwards = !body.forwards ? [] : (status === 'failed' || status === 'local_failed') ? body.forwards.slice(Math.max(0, body.forwards.length - 1000), Math.max(1000, body.forwards.length)).reverse() : body.forwards.reverse();
res.status(200).json(body.forwards);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Forwarding History Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const funderUpdatePolicy = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting or Updating Funder Policy..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/funderupdate';
options.body = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Funder Update Body', data: options.body });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Funder Policy Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Funder Policy Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

@ -1,60 +0,0 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { CLWSClient } from './webSocketClient.js';
let options = null;
const logger = Logger;
const common = Common;
const clWsClient = CLWSClient;
export const getInfo = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Getting Core Lightning Node Information..' });
common.logEnvVariables(req);
common.setOptions(req);
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/getinfo';
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Selected Node ' + req.session.selectedNode.lnNode });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Calling Info from Core Lightning server url ' + options.url });
if (!options.headers || !options.headers.rune) {
const errMsg = 'Core lightning get info failed due to missing rune!';
const err = common.handleError({ statusCode: 502, message: 'Bad rune', error: errMsg }, 'GetInfo', errMsg, req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
else {
return request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'GetInfo', msg: 'Node Information Before Update', data: body });
body.lnImplementation = 'Core Lightning';
const chainObj = { chain: '', network: '' };
if (body.network.includes('litecoin') || body.network.includes('feathercoin')) {
chainObj.chain = '';
chainObj.network = '';
}
else if (body.network.includes('liquid')) {
chainObj.chain = 'Liquid';
chainObj.network = common.titleCase(body.network);
}
else {
chainObj.chain = 'Bitcoin';
chainObj.network = common.titleCase(body.network);
}
body.chains = [chainObj];
body.uris = [];
if (body.address && body.address.length > 0) {
body.address.forEach((addr) => {
body.uris.push(body.id + '@' + addr.address + ':' + addr.port);
});
}
req.session.selectedNode.lnVersion = body.version || '';
req.session.selectedNode.api_version = body.api_version || '';
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Core Lightning\'s Websocket Server.' });
clWsClient.updateSelectedNode(req.session.selectedNode);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'GetInfo', 'Get Info Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
}
};

@ -1,55 +0,0 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
let options = null;
const logger = Logger;
const common = Common;
export const deleteExpiredInvoice = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Deleting Expired Invoices..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/delexpiredinvoice';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoices Deleted', data: body });
res.status(204).json({ status: 'Invoice Deleted Successfully' });
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'Delete Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const listInvoices = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting Invoices..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listinvoices';
options.body = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List URL', data: options.url });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'List Invoices Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const addInvoice = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/invoice';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Created', data: body });
res.status(201).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'Add Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

@ -1,99 +0,0 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
let options = null;
const logger = Logger;
const common = Common;
export const getRoute = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Getting Network Routes..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/getroute';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Network Routes Received', data: body });
return Promise.all(body.route?.map((rt) => getAlias(req.session.selectedNode, rt, 'id'))).then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Network Routes with Alias Received', data: body });
res.status(200).json(body || []);
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Network', 'Query Routes Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const listChannels = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Channel Lookup..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listchannels';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Channel Lookup Finished', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Network', 'Channel Lookup Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const feeRates = (req, res, next) => {
const { style } = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Getting Network Fee Rates..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/feerates';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'Network Fee Rates Received for ' + style, data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Network', 'Fee Rates Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const listNodes = (req, res, next) => {
const filter_liquidity_ads = !!req.body.liquidity_ads;
delete req.body.liquidity_ads;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'List Nodes..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listnodes';
options.body = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Network', msg: 'List Nodes URL' + options.url });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'List Nodes Finished', data: body });
let response = body.nodes;
if (filter_liquidity_ads) {
response = body.nodes.filter((node) => ((node.option_will_fund) ? node : null));
}
res.status(200).json(response);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Network', 'Node Lookup Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const getAlias = (selNode, peer, id) => {
options.url = selNode.settings.lnServerUrl + '/v1/listnodes';
if (!peer[id]) {
logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Network', msg: 'Empty Peer ID' });
peer.alias = '';
return peer;
}
options.body = { id: peer[id] };
return request.post(options).then((body) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Network', msg: 'Peer Alias Finished', data: body });
peer.alias = body.nodes[0] && body.nodes[0].alias ? body.nodes[0].alias : peer[id].substring(0, 20);
return peer;
}).catch((errRes) => {
common.handleError(errRes, 'Network', 'Peer Alias Error', selNode);
peer.alias = peer[id].substring(0, 20);
return peer;
});
};

@ -1,95 +0,0 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { Database } from '../../utils/database.js';
import { CollectionFieldsEnum, CollectionsEnum } from '../../models/database.model.js';
let options = null;
const logger = Logger;
const common = Common;
const databaseService = Database;
export const listOfferBookmarks = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offer Bookmarks..' });
databaseService.find(req.session.selectedNode, CollectionsEnum.OFFERS).then((offers) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmarks Received', data: offers });
res.status(200).json(offers);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'Offer Bookmarks Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const deleteOfferBookmark = (req, res, next) => {
const { offer_str } = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Deleting Offer Bookmark..' });
databaseService.remove(req.session.selectedNode, CollectionsEnum.OFFERS, CollectionFieldsEnum.BOLT12, offer_str).then((deleteRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmark Deleted', data: deleteRes });
res.status(204).json(offer_str);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'Offer Bookmark Delete Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const listOffers = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offers..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listoffers';
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Offers', msg: 'Offers List URL', data: options.url });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offers List Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'List Offers Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const createOffer = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Creating Offer..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/offer';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Created', data: body });
res.status(201).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Offer', 'Create Offer Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const fetchOfferInvoice = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offer Invoice..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/fetchinvoice';
options.body = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Offers', msg: 'Offer Invoice Body', data: options.body });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Invoice Received', data: body });
res.status(201).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'Get Offer Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const disableOffer = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Disabling Offer..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/disableOffer';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Disabled', data: body });
res.status(202).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'Disable Offer Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

@ -1,91 +0,0 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
let options = null;
const logger = Logger;
const common = Common;
export const getNewAddress = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Generating New Address..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/newaddr';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'New Address Generated', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'OnChain', 'New Address Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const onChainWithdraw = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Withdrawing from On Chain..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/withdraw';
options.body = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'OnChain', msg: 'OnChain Withdraw Options', data: options.body });
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Withdraw Finished', data: body });
res.status(201).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'OnChain', 'Withdraw Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const getUTXOs = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Listing Funds..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listfunds';
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Funds List Received', data: body });
// Local Remote Balance Calculation
let lrBalance = { localBalance: 0, remoteBalance: 0, inactiveBalance: 0, pendingBalance: 0 };
body.channels.forEach((channel) => {
if ((channel.state === 'CHANNELD_NORMAL') && channel.connected === true) {
lrBalance.localBalance = lrBalance.localBalance + channel.our_amount_msat;
lrBalance.remoteBalance = lrBalance.remoteBalance + (channel.amount_msat - channel.our_amount_msat);
}
else if ((channel.state === 'CHANNELD_NORMAL') && channel.connected === false) {
lrBalance.inactiveBalance = lrBalance.inactiveBalance + channel.our_amount_msat;
}
else if (channel.state === 'CHANNELD_AWAITING_LOCKIN' || channel.state === 'DUALOPEND_AWAITING_LOCKIN') {
lrBalance.pendingBalance = lrBalance.pendingBalance + channel.our_amount_msat;
}
});
lrBalance = {
localBalance: lrBalance.localBalance / 1000,
remoteBalance: lrBalance.remoteBalance / 1000,
inactiveBalance: lrBalance.inactiveBalance / 1000,
pendingBalance: lrBalance.pendingBalance / 1000
};
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Onchain', msg: 'Local Remote Balance', data: lrBalance });
// Onchain Balance Calculation
let onchainBalance = { totalBalance: 0, confBalance: 0, unconfBalance: 0 };
body.outputs.forEach((output) => {
if (output.status === 'confirmed') {
onchainBalance.confBalance = onchainBalance.confBalance + output.amount_msat;
}
else if (output.status === 'unconfirmed') {
onchainBalance.unconfBalance = onchainBalance.unconfBalance + output.amount_msat;
}
});
onchainBalance = {
totalBalance: onchainBalance.confBalance / 1000,
confBalance: (onchainBalance.confBalance - onchainBalance.unconfBalance) / 1000,
unconfBalance: onchainBalance.unconfBalance / 1000
};
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Onchain', msg: 'Onchain Balance Received', data: onchainBalance });
res.status(200).json({ utxos: body.outputs || [], balance: onchainBalance, localRemoteBalance: lrBalance });
}).catch((errRes) => {
const err = common.handleError(errRes, 'OnChain', 'List Funds Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

@ -1,188 +0,0 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { Database } from '../../utils/database.js';
import { CollectionFieldsEnum, CollectionsEnum } from '../../models/database.model.js';
let options = null;
const logger = Logger;
const common = Common;
const databaseService = Database;
export const getMemo = (selNode, payment) => {
options.url = selNode.settings.lnServerUrl + '/v1/decode';
options.body = { string: payment.bolt11 };
return request.post(options).then((res) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment Decode Received', data: res });
payment.memo = res.description || '';
return payment;
}).catch((err) => {
payment.memo = '';
return payment;
});
};
function paymentReducer(accumulator, currentPayment) {
const currPayHash = currentPayment.payment_hash;
if (!currentPayment.partid) {
currentPayment.partid = 0;
}
if (!accumulator[currPayHash]) {
accumulator[currPayHash] = [currentPayment];
}
else {
accumulator[currPayHash].push(currentPayment);
}
return accumulator;
}
function summaryReducer(accumulator, mpp) {
if (mpp.status === 'complete') {
accumulator.amount_msat = accumulator.amount_msat + mpp.amount_msat;
accumulator.amount_sent_msat = accumulator.amount_sent_msat + mpp.amount_sent_msat;
accumulator.status = mpp.status;
}
if (mpp.bolt11) {
accumulator.bolt11 = mpp.bolt11;
}
if (mpp.bolt12) {
accumulator.bolt12 = mpp.bolt12;
}
return accumulator;
}
function groupBy(payments) {
const paymentsInGroups = payments?.reduce(paymentReducer, {});
const paymentsGrpArray = Object.keys(paymentsInGroups)?.map((key) => ((paymentsInGroups[key].length && paymentsInGroups[key].length > 1) ? common.sortDescByKey(paymentsInGroups[key], 'partid') : paymentsInGroups[key]));
return paymentsGrpArray?.reduce((acc, curr) => {
let temp = {};
if (curr.length && curr.length === 1) {
temp = JSON.parse(JSON.stringify(curr[0]));
temp.is_group = false;
temp.is_expanded = false;
temp.total_parts = 1;
delete temp.partid;
}
else {
const paySummary = curr?.reduce(summaryReducer, { amount_msat: 0, amount_sent_msat: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' });
temp = {
is_group: true, is_expanded: false, total_parts: (curr.length ? curr.length : 0), status: paySummary.status, payment_hash: curr[0].payment_hash,
destination: curr[0].destination, amount_msat: paySummary.amount_msat, amount_sent_msat: paySummary.amount_sent_msat, created_at: curr[0].created_at,
mpps: curr
};
if (paySummary.bolt11) {
temp.bolt11 = paySummary.bolt11;
}
if (paySummary.bolt12) {
temp.bolt12 = paySummary.bolt12;
}
}
return acc.concat(temp);
}, []);
}
export const listPayments = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'List Payments..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listsendpays';
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: body.payments });
body.payments = body.payments && body.payments.length && body.payments.length > 0 ? groupBy(body.payments) : [];
return Promise.all(body.payments?.map((payment) => ((payment.bolt11) ? getMemo(req.session.selectedNode, payment) : (payment.memo = '')))).then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payments List with Memo Received', data: body.payments });
res.status(200).json(body.payments);
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const postPayment = (req, res, next) => {
const { paymentType, saveToDB, bolt12, zeroAmtOffer, amount_msat, title, issuer, description } = req.body;
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
const options_body = JSON.parse(JSON.stringify(req.body));
if (paymentType === 'KEYSEND') {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Keysend Payment..' });
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/keysend';
delete options_body.uiMessage;
delete options_body.fromDialog;
delete options_body.paymentType;
delete options_body.title;
delete options_body.issuer;
delete options_body.bolt11;
delete options_body.description;
delete options_body.bolt12;
delete options_body.zeroAmtOffer;
delete options_body.pubkey;
delete options_body.riskfactor;
delete options_body.localinvreqid;
delete options_body.exclude;
delete options_body.maxfee;
delete options_body.saveToDB;
options.body = options_body;
}
else {
if (paymentType === 'OFFER') {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sending Offer Payment..' });
}
else {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sending Invoice Payment..' });
}
if (paymentType === 'OFFER') {
// delete amount for zero amt offer also as fetchinvoice already has amount information
delete options_body.amount_msat;
}
delete options_body.uiMessage;
delete options_body.fromDialog;
delete options_body.paymentType;
delete options_body.destination;
delete options_body.extratlvs;
delete options_body.title;
delete options_body.issuer;
delete options_body.bolt12;
delete options_body.zeroAmtOffer;
delete options_body.pubkey;
delete options_body.saveToDB;
options.body = options_body;
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/pay';
}
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent', data: body });
if (paymentType === 'OFFER') {
if (saveToDB && bolt12) {
const offerToUpdate = { bolt12: bolt12, amountMSat: (zeroAmtOffer ? 0 : amount_msat), title: title, lastUpdatedAt: new Date(Date.now()).getTime() };
if (issuer) {
offerToUpdate['issuer'] = issuer;
}
if (description) {
offerToUpdate['description'] = description;
}
// eslint-disable-next-line arrow-body-style
return databaseService.validateDocument(CollectionsEnum.OFFERS, offerToUpdate).then((validated) => {
return databaseService.update(req.session.selectedNode, CollectionsEnum.OFFERS, offerToUpdate, CollectionFieldsEnum.BOLT12, bolt12).then((updatedOffer) => {
logger.log({ level: 'DEBUG', fileName: 'Payments', msg: 'Offer Updated', data: updatedOffer });
return res.status(201).json({ paymentResponse: body, saveToDBResponse: updatedOffer });
}).catch((errDB) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB update error', error: errDB });
return res.status(201).json({ paymentResponse: body, saveToDBError: errDB });
});
}).catch((errValidation) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB validation error', error: errValidation });
return res.status(201).json({ paymentResponse: body, saveToDBError: errValidation });
});
}
else {
return res.status(201).json({ paymentResponse: body, saveToDBResponse: 'NA' });
}
}
if (paymentType === 'INVOICE') {
return res.status(201).json({ paymentResponse: body, saveToDBResponse: 'NA' });
}
if (paymentType === 'KEYSEND') {
return res.status(201).json(body);
}
}).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

@ -1,67 +0,0 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { getAlias } from './network.js';
let options = null;
const logger = Logger;
const common = Common;
export const getPeers = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'List Peers..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listpeers';
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers List Received', data: body });
const peers = !body.peers ? [] : body.peers;
return Promise.all(peers?.map((peer) => getAlias(req.session.selectedNode, peer, 'id'))).then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body.peers });
res.status(200).json(body.peers || []);
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Peers', 'List Peers Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const postPeer = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Connecting Peer..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/connect';
options.body = req.body;
request.post(options).then((connectRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: connectRes });
const listOptions = common.getOptions(req);
listOptions.url = req.session.selectedNode.settings.lnServerUrl + '/v1/listpeers';
request.post(listOptions).then((listPeersRes) => {
const peers = listPeersRes && listPeersRes.peers ? common.newestOnTop(listPeersRes.peers, 'id', connectRes.id) : [];
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers });
res.status(201).json(peers);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Peers', 'Connect Peer Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const deletePeer = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Disconnecting Peer..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/disconnect';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peer Disconnected', data: body });
res.status(204).json({});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Peers', 'Detach Peer Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save