Merge branch 'Release-0.14.0' into copy-fix

pull/1205/head
ShahanaFarooqui 12 months ago committed by GitHub
commit 23ff7ac1ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -200,6 +200,7 @@
"@angular-eslint/template/no-autofocus": "off", "@angular-eslint/template/no-autofocus": "off",
"@angular-eslint/template/no-call-expression": "off", "@angular-eslint/template/no-call-expression": "off",
"@angular-eslint/template/no-inline-styles": "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/no-positive-tabindex": "off",
"@angular-eslint/template/use-track-by-function": "off" "@angular-eslint/template/use-track-by-function": "off"
} }

2
.github/README.md vendored

@ -35,6 +35,7 @@ RTL is available on the below platforms/services:
* [BCubium](https://bgeometrics.com) * [BCubium](https://bgeometrics.com)
* [Start9Labs](https://start9labs.com) * [Start9Labs](https://start9labs.com)
* [Umbrel](https://github.com/getumbrel/umbrel) * [Umbrel](https://github.com/getumbrel/umbrel)
* [Sovran Systems](https://sovransystems.com)
Docker Image: https://hub.docker.com/r/shahanafarooqui/rtl Docker Image: https://hub.docker.com/r/shahanafarooqui/rtl
@ -83,6 +84,7 @@ Example RTL-Config.json:
"multiPass": "password", "multiPass": "password",
"port": "3000", "port": "3000",
"defaultNodeIndex": 1, "defaultNodeIndex": 1,
"dbDirectoryPath": "<Complete path of the folder where rtl's database file should be saved>",
"SSO": { "SSO": {
"rtlSSO": 0, "rtlSSO": 0,
"rtlCookiePath": "", "rtlCookiePath": "",

@ -9,16 +9,17 @@ parameters have `default` values for initial setup and can be updated after RTL
"port": "<port number for the rtl node server, default '3000', Required>", "port": "<port number for the rtl node server, default '3000', Required>",
"host": "<host for the rtl node server, default 'all IPs', Optional>", "host": "<host for the rtl node server, default 'all IPs', Optional>",
"defaultNodeIndex": <Default index to load when rtl server starts, default 1, 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": { "SSO": {
"rtlSSO": <parameter to turn SSO off/on. Allowed values - 1 (single sign on via an external cookie), 0 (stand alone RTL authentication), default 0, Required>, "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)>", "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)>" "logoutRedirectLink": "<URL to re-direct to after logout/timeout from RTL, Required if SSO=1 else empty (Optional)>"
}, },
"nodes": [ "nodes": [
{ {
"index": <Incremental node indices starting from 1, Required>, "index": <Incremental node indices starting from 1, Required>,
"lnNode": "<Node name to uniquely identify the node in the UI, Default 'Node 1', Required>", "lnNode": "<Node name to uniquely identify the node in the UI, Required>",
"lnImplementation": "<LNP implementation, Allowed values LND/CLN/ECL. Default 'LND', Required>", "lnImplementation": "<LNP implementation, Allowed values LND/CLN/ECL, Required>",
"Authentication": { "Authentication": {
"macaroonPath": "<Path for the folder containing 'admin.macaroon' (LND)/'access.macaroon' (CLN) file, Required for LND & CLN>", "macaroonPath": "<Path for the folder containing 'admin.macaroon' (LND)/'access.macaroon' (CLN) file, Required for LND & CLN>",
"swapMacaroonPath": "<Path for the folder containing 'loop.macaroon' (LND), Required for LND Loop>", "swapMacaroonPath": "<Path for the folder containing 'loop.macaroon' (LND), Required for LND Loop>",
@ -27,16 +28,16 @@ parameters have `default` values for initial setup and can be updated after RTL
"lnApiPassword": "<Password to be used for ECL API authentication. Mandatory only for ECL if the configPath is missing>" "lnApiPassword": "<Password to be used for ECL API authentication. Mandatory only for ECL if the configPath is missing>"
}, },
"Settings": { "Settings": {
"userPersona": "<User persona to tailor the data on UI. Allowed values MERCHANT, OPERATOR. Default MERCHANT, Required>", "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, Required>", "themeMode": "<Theme modes, Allowed values DAY, NIGHT. Default DAY, Optional>",
"themeColor": "<Theme colors, Allowed values PURPLE, TEAL, INDIGO, PINK, YELLOW. Default PURPLE, Required>", "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>", "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>", "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>, "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, Required>, "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' If fiatConversion is true, Required if fiatConversion is true>", "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> "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', Required", "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>", "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>" "boltzServerUrl": "<Service url for boltz server REST APIs for the node, e.g. https://127.0.0.1:9003, Optional>"
} }
@ -49,11 +50,12 @@ parameters have `default` values for initial setup and can be updated after RTL
The environment variable can also be used for all of the above configurations except the UI settings.<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 /> If the environment variables are set, it will take precedence over the parameters in the RTL-Config.json file.<br />
<br /> <br />
PORT (port number for the rtl node server, default 3000, Required)<br /> PORT (port number for the rtl node server, default 3000, Optional)<br />
HOST (host for the rtl node server, default localhost, 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 /> 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', Required)<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) (Required)<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 /> 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 /> 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 /> 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 />

@ -65,6 +65,7 @@ Ensure that the follow values are correct per your config:
"multiPass": <password required for accessing RTL>, "multiPass": <password required for accessing RTL>,
"port": "3000", "port": "3000",
"defaultNodeIndex": 1, "defaultNodeIndex": 1,
"dbDirectoryPath": "<Complete path of the folder where rtl's database file should be saved>",
"SSO": { "SSO": {
"rtlSSO": 0, "rtlSSO": 0,
"rtlCookiePath": "", "rtlCookiePath": "",

@ -60,6 +60,7 @@ Ensure that the follow values are correct per your config:
"multiPass": <password required for accessing RTL>, "multiPass": <password required for accessing RTL>,
"port": "3000", "port": "3000",
"defaultNodeIndex": 1, "defaultNodeIndex": 1,
"dbDirectoryPath": "<Complete path of the folder where rtl's database file should be saved>",
"SSO": { "SSO": {
"rtlSSO": 0, "rtlSSO": 0,
"rtlCookiePath": "", "rtlCookiePath": "",

@ -20,16 +20,17 @@ This step is only required to configure the nodes, which will be remotely connec
2. Set `multiPass` to the preferred password. This password will be used to authenticate the user for RTL. Once authenticated, the user will be able to access all the nodes configured in the json file 2. Set `multiPass` to the preferred password. This password will be used to authenticate the user for RTL. Once authenticated, the user will be able to access all the nodes configured in the json file
3. Set the `port` to the preferred port number over which to run RTL 3. Set the `port` to the preferred port number over which to run RTL
4. Set the `defaultNodeIndex` to configure the default start up node at server restart 4. Set the `defaultNodeIndex` to configure the default start up node at server restart
5. `SSO` section can be used for single-sign-on from applications like BTCPayserver. If using RTL as a stand-alone app to connect with the nodes, keep the `rtlSSO=0` and ignore the rest of `SSO` section. 5. `dbDirectoryPath` should be set to the folder where RTL's database will be saved.
6. `nodes` section is a json array, with each element of the array representing the specific parameters for the LND node to connect with. `index` must be a number and start with 1. This number must be unique for each node in the array. For each element, two items need to be configured for each node on the network (`macaroonPath` and `lnServerUrl`). 6. `SSO` section can be used for single-sign-on from applications like BTCPayserver. If using RTL as a stand-alone app to connect with the nodes, keep the `rtlSSO=0` and ignore the rest of `SSO` section.
7. `macaroonPath` should be set to the local path of the folder containing `admin.macaroon` file for each node. Each node must have a different folder for the `admin.macaroon` on the RTL server. 7. `nodes` section is a json array, with each element of the array representing the specific parameters for the LND node to connect with. `index` must be a number and start with 1. This number must be unique for each node in the array. For each element, two items need to be configured for each node on the network (`macaroonPath` and `lnServerUrl`).
8. `swapMacaroonPath` should be set to the local path of the folder containing `loop.macaroon` file for loop. 8. `macaroonPath` should be set to the local path of the folder containing `admin.macaroon` file for each node. Each node must have a different folder for the `admin.macaroon` on the RTL server.
9. `boltzMacaroonPath` should be set to the local path of the folder containing `admin.macaroon` file for boltz swaps. 9. `swapMacaroonPath` should be set to the local path of the folder containing `loop.macaroon` file for loop.
10. `lnServerUrl` must be set to the service url for LND/Core Lightining REST APIs for each node, with the unique ip address of the node hosting LND/Core Lightning e.g. https://192.168.0.1:8080 OR https://192.168.0.1:3001. In this case the ip address of the node hosting LND/Core Lightning is '192.168.0.1' 10. `boltzMacaroonPath` should be set to the local path of the folder containing `admin.macaroon` file for boltz swaps.
11. `swapServerUrl` must be set to the swap service url. e.g. https://127.0.0.1:8081. 11. `lnServerUrl` must be set to the service url for LND/Core Lightining REST APIs for each node, with the unique ip address of the node hosting LND/Core Lightning e.g. https://192.168.0.1:8080 OR https://192.168.0.1:3001. In this case the ip address of the node hosting LND/Core Lightning is '192.168.0.1'
12. `boltzServerUrl` must be set to the boltz service url. e.g. https://127.0.0.1:9003. 12. `swapServerUrl` must be set to the swap service url. e.g. https://127.0.0.1:8081.
13. `configPath` and `bitcoindConfigPath` are optional parameters which can be set only if the RTL is running locally on the same node. Else it can be set to "" or removed from the conf file all together. 13. `boltzServerUrl` must be set to the boltz service url. e.g. https://127.0.0.1:9003.
14. `lnApiPassword` is mandatory in the ln implementation is ECL and configPath is missing. It is used to provide password for API authentication. It will be ignored in other ln implementations. 14. `configPath` and `bitcoindConfigPath` are optional parameters which can be set only if the RTL is running locally on the same node. Else it can be set to "" or removed from the conf file all together.
15. `lnApiPassword` is mandatory in the ln implementation is ECL and configPath is missing. It is used to provide password for API authentication. It will be ignored in other ln implementations.
#### 3. Restart RTL #### 3. Restart RTL

@ -15,6 +15,7 @@ If your running RTL and LND on different devices on your local LAN, certain conf
"multiPass": "<password in plain text, Default 'password'>", "multiPass": "<password in plain text, Default 'password'>",
"port": "3000", "port": "3000",
"defaultNodeIndex": 1, "defaultNodeIndex": 1,
"dbDirectoryPath": "<Complete path of the folder where rtl's database file should be saved>",
"SSO": { "SSO": {
"rtlSSO": 0, "rtlSSO": 0,
"rtlCookiePath": "", "rtlCookiePath": "",

@ -1,23 +0,0 @@
name: Pull Request Stats
on:
push:
branches: [ master, 'Release-*' ]
tags: [ 'v*' ]
release:
types: [released]
# Triggers the workflow only when merging pull request to the branches.
pull_request:
types: [opened, closed]
branches: [ master, 'Release-*', '*' ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
stats:
runs-on: ubuntu-latest
steps:
- name: Run pull request stats
uses: flowwer-dev/pull-request-stats@master
with:
period: 365

@ -2,6 +2,7 @@
"multiPass": "password", "multiPass": "password",
"port": "3000", "port": "3000",
"defaultNodeIndex": 1, "defaultNodeIndex": 1,
"dbDirectoryPath": "C:\\Users\\xyz\\RTL",
"SSO": { "SSO": {
"rtlSSO": 0, "rtlSSO": 0,
"rtlCookiePath": "", "rtlCookiePath": "",

@ -22,7 +22,8 @@
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": [ "polyfills": [
"zone.js" "zone.js",
"src/polyfills.ts"
], ],
"tsConfig": "src/tsconfig.app.json", "tsConfig": "src/tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
@ -34,6 +35,7 @@
"node_modules/material-icons/iconfont/material-icons.css", "node_modules/material-icons/iconfont/material-icons.css",
"node_modules/roboto-fontface/css/roboto/roboto-fontface.css" "node_modules/roboto-fontface/css/roboto/roboto-fontface.css"
], ],
"scripts": [],
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"buffer", "buffer",
"rfdc", "rfdc",
@ -41,10 +43,8 @@
"qrcode", "qrcode",
"otplib", "otplib",
"pdfmake/build/pdfmake", "pdfmake/build/pdfmake",
"pdfmake/build/vfs_fonts", "pdfmake/build/vfs_fonts"
"clone-deep" ]
],
"scripts": []
}, },
"configurations": { "configurations": {
"production": { "production": {
@ -96,7 +96,8 @@
"options": { "options": {
"polyfills": [ "polyfills": [
"zone.js", "zone.js",
"zone.js/testing" "zone.js/testing",
"src/polyfills.ts"
], ],
"tsConfig": "src/tsconfig.spec.json", "tsConfig": "src/tsconfig.spec.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
@ -119,14 +120,11 @@
"src/**/*.html" "src/**/*.html"
] ]
} }
} }
} }
} }
}, },
"cli": { "cli": {
"schematicCollections": [
"@angular-eslint/schematics"
],
"analytics": false "analytics": false
} }
} }

@ -4,21 +4,48 @@ import { Common } from '../../utils/common.js';
let options = null; let options = null;
const logger = Logger; const logger = Logger;
const common = Common; 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.ln_server_url + '/v1/channel/listPeerChannels';
request(options).then((body) => {
body?.map((channel) => {
if (!channel.alias || channel.alias === '') {
channel.alias = channel.peer_id.substring(0, 20);
}
const local = channel.to_us_msat || 0;
const remote = (channel.total_msat - local) || 0;
const total = channel.total_msat || 0;
channel.to_them_msat = remote;
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
return channel;
});
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Peer Channels List Received', data: body });
res.status(200).json(body);
}).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 listChannels = (req, res, next) => { export const listChannels = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channels..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channels..' });
options = common.getOptions(req); options = common.getOptions(req);
if (options.error) { if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error }); return res.status(options.statusCode).json({ message: options.message, error: options.error });
} }
options.url = req.session.selectedNode.ln_server_url + '/v1/channel/listChannels'; options.url = req.session.selectedNode.ln_server_url + '/v1/channel/listPeerChannels';
request(options).then((body) => { request(options).then((body) => {
body?.map((channel) => { body?.map((channel) => {
if (!channel.alias || channel.alias === '') { if (!channel.alias || channel.alias === '') {
channel.alias = channel.id.substring(0, 20); channel.alias = channel.id.substring(0, 20);
} }
const local = (channel.msatoshi_to_us) ? channel.msatoshi_to_us : 0; const local = channel.to_us_msat || 0;
const remote = (channel.msatoshi_to_them) ? channel.msatoshi_to_them : 0; const remote = (channel.total_msat - local) || 0;
const total = channel.msatoshi_total ? channel.msatoshi_total : 0; const total = channel.total_msat || 0;
channel.to_them_msat = remote;
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3); channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
return channel; return channel;
}); });

@ -22,8 +22,8 @@ function paymentReducer(accumulator, currentPayment) {
} }
function summaryReducer(accumulator, mpp) { function summaryReducer(accumulator, mpp) {
if (mpp.status === 'complete') { if (mpp.status === 'complete') {
accumulator.msatoshi = accumulator.msatoshi + mpp.msatoshi; accumulator.amount_msat = accumulator.amount_msat + mpp.amount_msat;
accumulator.msatoshi_sent = accumulator.msatoshi_sent + mpp.msatoshi_sent; accumulator.amount_sent_msat = accumulator.amount_sent_msat + mpp.amount_sent_msat;
accumulator.status = mpp.status; accumulator.status = mpp.status;
} }
if (mpp.bolt11) { if (mpp.bolt11) {
@ -47,10 +47,10 @@ function groupBy(payments) {
delete temp.partid; delete temp.partid;
} }
else { else {
const paySummary = curr?.reduce(summaryReducer, { msatoshi: 0, msatoshi_sent: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' }); const paySummary = curr?.reduce(summaryReducer, { amount_msat: 0, amount_sent_msat: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' });
temp = { temp = {
is_group: true, is_expanded: false, total_parts: (curr.length ? curr.length : 0), status: paySummary.status, payment_hash: curr[0].payment_hash, 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, msatoshi: paySummary.msatoshi, msatoshi_sent: paySummary.msatoshi_sent, created_at: curr[0].created_at, destination: curr[0].destination, amount_msat: paySummary.amount_msat, amount_sent_msat: paySummary.amount_sent_msat, created_at: curr[0].created_at,
mpps: curr mpps: curr
}; };
if (paySummary.bolt11) { if (paySummary.bolt11) {
@ -104,8 +104,8 @@ export const postPayment = (req, res, next) => {
if (req.body.paymentType === 'OFFER') { if (req.body.paymentType === 'OFFER') {
if (req.body.saveToDB && req.body.bolt12) { if (req.body.saveToDB && req.body.bolt12) {
const offerToUpdate = { bolt12: req.body.bolt12, amountMSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() }; const offerToUpdate = { bolt12: req.body.bolt12, amountMSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() };
if (req.body.vendor) { if (req.body.issuer) {
offerToUpdate['vendor'] = req.body.vendor; offerToUpdate['issuer'] = req.body.issuer;
} }
if (req.body.description) { if (req.body.description) {
offerToUpdate['description'] = req.body.description; offerToUpdate['description'] = req.body.description;

@ -72,17 +72,12 @@ export class CLWebSocketClient {
this.wsServer.sendEventsToAllLNClients(msgStr, clWsClt.selectedNode); this.wsServer.sendEventsToAllLNClients(msgStr, clWsClt.selectedNode);
}; };
clWsClt.webSocketClient.onerror = (err) => { clWsClt.webSocketClient.onerror = (err) => {
if (clWsClt.selectedNode.api_version === '' || !clWsClt.selectedNode.api_version || this.common.isVersionCompatible(clWsClt.selectedNode.api_version, '0.6.0')) { this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'ERROR', fileName: 'CLWebSocket', msg: 'Web socket error', error: err });
this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'ERROR', fileName: 'CLWebSocket', msg: 'Web socket error', error: err }); const errStr = ((typeof err === 'object' && err.message) ? JSON.stringify({ error: err.message }) : (typeof err === 'object') ? JSON.stringify({ error: err }) : ('{ "error": ' + err + ' }'));
const errStr = ((typeof err === 'object' && err.message) ? JSON.stringify({ error: err.message }) : (typeof err === 'object') ? JSON.stringify({ error: err }) : ('{ "error": ' + err + ' }')); this.wsServer.sendErrorToAllLNClients(errStr, clWsClt.selectedNode);
this.wsServer.sendErrorToAllLNClients(errStr, clWsClt.selectedNode); clWsClt.webSocketClient.close();
clWsClt.webSocketClient.close(); if (clWsClt.reConnect) {
if (clWsClt.reConnect) { this.reconnet(clWsClt);
this.reconnet(clWsClt);
}
}
else {
clWsClt.reConnect = false;
} }
}; };
}; };

@ -1,6 +1,9 @@
import request from 'request-promise'; import request from 'request-promise';
import { Logger } from '../../utils/logger.js'; import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js'; import { Common } from '../../utils/common.js';
import { createInvoiceRequestCall, listPendingInvoicesRequestCall } from './invoices.js';
import { findRouteBetweenNodesRequestCall } from './network.js';
import { getSentInfoFromPaymentRequest, sendPaymentToRouteRequestCall } from './payments.js';
let options = null; let options = null;
const logger = Logger; const logger = Logger;
const common = Common; const common = Common;
@ -156,3 +159,54 @@ export const closeChannel = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error }); return res.status(err.statusCode).json({ message: err.message, error: err.error });
}); });
}; };
// options.form = { sourceNodeId: req.params.source, targetNodeId: req.params.target, amountMsat: req.params.amount, ignoreNodeIds: req.params.ignore };
export const circularRebalance = (req, res, next) => {
const crInvDescription = 'Circular rebalancing invoice for ' + (req.body.amountMsat / 1000) + ' Sats';
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.form = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Rebalance Params', data: options.form });
const tillToday = (Math.round(new Date(Date.now()).getTime() / 1000)).toString();
// Check if unpaid Invoice exists already
listPendingInvoicesRequestCall(req.session.selectedNode).then((callRes) => {
const foundExistingInvoice = callRes.find((inv) => inv.description.includes(crInvDescription) && inv.amount === req.body.amountMsat && inv.expiry && inv.timestamp && ((inv.expiry + inv.timestamp) >= tillToday));
// Create new invoice if doesn't exist already
const requestCalls = foundExistingInvoice && foundExistingInvoice.serialized ?
[findRouteBetweenNodesRequestCall(req.session.selectedNode, req.body.amountMsat, req.body.sourceNodeId, req.body.targetNodeId, req.body.ignoreNodeIds, req.body.format)] :
[findRouteBetweenNodesRequestCall(req.session.selectedNode, req.body.amountMsat, req.body.sourceNodeId, req.body.targetNodeId, req.body.ignoreNodeIds, req.body.format), createInvoiceRequestCall(req.session.selectedNode, crInvDescription, req.body.amountMsat)];
Promise.all(requestCalls).then((values) => {
// eslint-disable-next-line arrow-body-style
const routes = values[0]?.routes?.filter((route) => {
return !((route.shortChannelIds[0] === req.body.sourceShortChannelId && route.shortChannelIds[1] === req.body.targetShortChannelId) ||
(route.shortChannelIds[1] === req.body.sourceShortChannelId && route.shortChannelIds[0] === req.body.targetShortChannelId));
});
const firstRoute = routes[0].shortChannelIds.join() || '';
const shortChannelIds = req.body.sourceShortChannelId + ',' + firstRoute + ',' + req.body.targetShortChannelId;
const invoice = (foundExistingInvoice && foundExistingInvoice.serialized ? foundExistingInvoice.serialized : (values[1] ? values[1].serialized : '')) || '';
const paymentHash = (foundExistingInvoice && foundExistingInvoice.paymentHash ? foundExistingInvoice.paymentHash : (values[1] ? values[1].paymentHash : '') || '');
return sendPaymentToRouteRequestCall(req.session.selectedNode, shortChannelIds, invoice, req.body.amountMsat).then((payToRouteCallRes) => {
// eslint-disable-next-line arrow-body-style
setTimeout(() => {
return getSentInfoFromPaymentRequest(req.session.selectedNode, paymentHash).then((sentInfoCallRes) => {
const payStatus = sentInfoCallRes.length && sentInfoCallRes.length > 0 ? sentInfoCallRes[sentInfoCallRes.length - 1].status : sentInfoCallRes;
return res.status(201).json({ flgReusingInvoice: !!foundExistingInvoice, invoice: invoice, paymentHash: paymentHash, paymentDetails: payToRouteCallRes, paymentStatus: payStatus });
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Channel Rebalance From Sent Info Error', req.session.selectedNode);
return res.status(err.statusCode).json({ flgReusingInvoice: !!foundExistingInvoice, invoice: invoice, paymentHash: paymentHash, paymentDetails: payToRouteCallRes, paymentStatus: err.error });
});
}, 3000);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Channel Rebalance From Send Payment To Route Error', req.session.selectedNode);
return res.status(err.statusCode).json({ flgReusingInvoice: !!foundExistingInvoice, invoice: invoice, paymentHash: paymentHash, paymentDetails: {}, paymentStatus: err.error });
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Channel Rebalance From Find Routes Error', req.session.selectedNode);
return res.status(err.statusCode).json({ flgReusingInvoice: !!foundExistingInvoice, invoice: (foundExistingInvoice.serialized || ''), paymentHash: '', paymentDetails: {}, paymentStatus: err.error });
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Channel Rebalance From List Pending Invoices Error', req.session.selectedNode);
return res.status(err.statusCode).json({ flgReusingInvoice: false, invoice: '', paymentHash: '', paymentDetails: {}, paymentStatus: err.error });
});
};

@ -19,7 +19,7 @@ export const getReceivedPaymentInfo = (lnServerUrl, invoice) => {
invoice.status = response.status.type; invoice.status = response.status.type;
if (response.status && response.status.type === 'received') { if (response.status && response.status.type === 'received') {
invoice.amountSettled = response.status.amount ? Math.round(response.status.amount / 1000) : 0; invoice.amountSettled = response.status.amount ? Math.round(response.status.amount / 1000) : 0;
invoice.receivedAt = response.status.receivedAt ? Math.round(response.status.receivedAt / 1000) : 0; invoice.receivedAt = response.status.receivedAt.unix ? response.status.receivedAt.unix : 0;
} }
return invoice; return invoice;
}).catch((err) => { }).catch((err) => {
@ -53,6 +53,20 @@ export const getInvoice = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error }); return res.status(err.statusCode).json({ message: err.message, error: err.error });
}); });
}; };
export const listPendingInvoicesRequestCall = (selectedNode) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'List Pending Invoices..' });
options = selectedNode.options;
options.url = selectedNode.ln_server_url + '/listpendinginvoices';
options.form = { from: 0, to: (Math.round(new Date(Date.now()).getTime() / 1000)).toString() };
return new Promise((resolve, reject) => {
request.post(options).then((pendingInvoicesResponse) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Pending Invoices List ', data: pendingInvoicesResponse });
resolve(pendingInvoicesResponse);
}).catch((errRes) => {
reject(common.handleError(errRes, 'Invoices', 'List Pending Invoices Error', selectedNode));
});
});
};
export const listInvoices = (req, res, next) => { export const listInvoices = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting List Invoices..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting List Invoices..' });
options = common.getOptions(req); options = common.getOptions(req);
@ -103,22 +117,30 @@ export const listInvoices = (req, res, next) => {
}); });
} }
}; };
export const createInvoiceRequestCall = (selectedNode, description, amount) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = selectedNode.options;
options.url = selectedNode.ln_server_url + '/createinvoice';
options.form = { description: description, amountMsat: amount };
return new Promise((resolve, reject) => {
request.post(options).then((invResponse) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Created', data: invResponse });
if (invResponse.amount) {
invResponse.amount = Math.round(invResponse.amount / 1000);
}
resolve(invResponse);
}).catch((errRes) => {
reject(common.handleError(errRes, 'Invoices', 'Create Invoice Error', selectedNode));
});
});
};
export const createInvoice = (req, res, next) => { export const createInvoice = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = common.getOptions(req); options = common.getOptions(req);
if (options.error) { if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error }); return res.status(options.statusCode).json({ message: options.message, error: options.error });
} }
options.url = req.session.selectedNode.ln_server_url + '/createinvoice'; createInvoiceRequestCall(req.session.selectedNode, req.body.description, req.body.amountMsat).then((invRes) => {
options.form = req.body; res.status(201).json(invRes);
request.post(options).then((body) => { }).catch((err) => res.status(err.statusCode).json({ message: err.message, error: err.error }));
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Created', data: body });
if (body.amount) {
body.amount = Math.round(body.amount / 1000);
}
res.status(201).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoices', 'Create Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
}; };

@ -20,3 +20,26 @@ export const getNodes = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error }); return res.status(err.statusCode).json({ message: err.message, error: err.error });
}); });
}; };
export const findRouteBetweenNodesRequestCall = (selectedNode, amountMsat, sourceNodeId, targetNodeId, ignoreNodeIds = [], format = 'shortChannelId') => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Network', msg: 'Find Route Between Nodes..' });
options = selectedNode.options;
options.url = selectedNode.ln_server_url + '/findroutebetweennodes';
options.form = { amountMsat: amountMsat, sourceNodeId: sourceNodeId, targetNodeId: targetNodeId, ignoreNodeIds: ignoreNodeIds, format: format };
return new Promise((resolve, reject) => {
request.post(options).then((body) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Network', msg: 'Route Lookup Between Nodes Finished', data: body });
resolve(body);
}).catch((errRes) => {
reject(common.handleError(errRes, 'Network', 'Route Lookup Between Nodes Error', selectedNode));
});
});
};
export const findRouteBetweenNodes = (req, res, next) => {
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
findRouteBetweenNodesRequestCall(req.session.selectedNode, req.body.amountMsat, req.body.sourceNodeId, req.body.targetNodeId, req.body.ignoreNodeIds, req.body.format).then((callRes) => {
res.status(200).json(callRes);
}).catch((err) => res.status(err.statusCode).json({ message: err.message, error: err.error }));
};

@ -126,3 +126,28 @@ export const getSentPaymentsInformation = (req, res, next) => {
return res.status(200).json([]); return res.status(200).json([]);
} }
}; };
export const sendPaymentToRouteRequestCall = (selectedNode, shortChannelIds, invoice, amountMsat) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = selectedNode.options;
options.url = selectedNode.ln_server_url + '/sendtoroute';
options.form = { shortChannelIds: shortChannelIds, amountMsat: amountMsat, invoice: invoice };
return new Promise((resolve, reject) => {
logger.log({ selectedNode: selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Send Payment To Route Options', data: options.form });
request.post(options).then((body) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent To Route', data: body });
resolve(body);
}).catch((errRes) => {
reject(common.handleError(errRes, 'Payments', 'Send Payment To Route Error', selectedNode));
});
});
};
export const sendPaymentToRoute = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Send Payment To Route..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
sendPaymentToRouteRequestCall(req.session.selectedNode, req.body.shortChannelIds, req.body.invoice, req.body.amountMsat).then((callRes) => {
res.status(200).json(callRes);
}).catch((err) => res.status(err.statusCode).json({ message: err.message, error: err.error }));
};

@ -110,10 +110,12 @@ export class ECLWebSocketClient {
}; };
this.heartbeat = (eclWsClt) => { this.heartbeat = (eclWsClt) => {
this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Websocket Server Heartbeat..' }); this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Websocket Server Heartbeat..' });
if (!eclWsClt.webSocketClient) if (!eclWsClt.webSocketClient) {
return; return;
if (eclWsClt.webSocketClient.readyState !== 1) }
if (eclWsClt.webSocketClient.readyState !== 1) {
return; return;
}
eclWsClt.webSocketClient.ping(); eclWsClt.webSocketClient.ping();
setTimeout(() => { setTimeout(() => {
this.heartbeat(eclWsClt); this.heartbeat(eclWsClt);

@ -176,6 +176,7 @@ export const postTransactions = (req, res, next) => {
options.form = JSON.stringify(options.form); options.form = JSON.stringify(options.form);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form }); logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form });
request.post(options).then((body) => { request.post(options).then((body) => {
body = body.result ? body.result : body;
if (body.payment_error) { if (body.payment_error) {
const err = common.handleError(body.payment_error, 'Channels', 'Send Payment Error', req.session.selectedNode); const err = common.handleError(body.payment_error, 'Channels', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error }); return res.status(err.statusCode).json({ message: err.message, error: err.error });

@ -3,15 +3,15 @@ export var OfferFieldsEnum;
OfferFieldsEnum["BOLT12"] = "bolt12"; OfferFieldsEnum["BOLT12"] = "bolt12";
OfferFieldsEnum["AMOUNTMSAT"] = "amountMSat"; OfferFieldsEnum["AMOUNTMSAT"] = "amountMSat";
OfferFieldsEnum["TITLE"] = "title"; OfferFieldsEnum["TITLE"] = "title";
OfferFieldsEnum["VENDOR"] = "vendor"; OfferFieldsEnum["ISSUER"] = "issuer";
OfferFieldsEnum["DESCRIPTION"] = "description"; OfferFieldsEnum["DESCRIPTION"] = "description";
})(OfferFieldsEnum || (OfferFieldsEnum = {})); })(OfferFieldsEnum || (OfferFieldsEnum = {}));
export class Offer { export class Offer {
constructor(bolt12, amountMSat, title, vendor, description, lastUpdatedAt) { constructor(bolt12, amountMSat, title, issuer, description, lastUpdatedAt) {
this.bolt12 = bolt12; this.bolt12 = bolt12;
this.amountMSat = amountMSat; this.amountMSat = amountMSat;
this.title = title; this.title = title;
this.vendor = vendor; this.issuer = issuer;
this.description = description; this.description = description;
this.lastUpdatedAt = lastUpdatedAt; this.lastUpdatedAt = lastUpdatedAt;
} }

@ -1,9 +1,10 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; import { isAuthenticated } from '../../utils/authCheck.js';
import { listChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards, funderUpdatePolicy, listForwardsPaginated } from '../../controllers/cln/channels.js'; import { listChannels, listPeerChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards, funderUpdatePolicy, listForwardsPaginated } from '../../controllers/cln/channels.js';
const router = Router(); const router = Router();
router.get('/listChannels', isAuthenticated, listChannels); router.get('/listChannels', isAuthenticated, listChannels);
router.get('/listPeerChannels', isAuthenticated, listPeerChannels);
router.post('/', isAuthenticated, openChannel); router.post('/', isAuthenticated, openChannel);
router.post('/setChannelFee', isAuthenticated, setChannelFee); router.post('/setChannelFee', isAuthenticated, setChannelFee);
router.delete('/:channelId', isAuthenticated, closeChannel); router.delete('/:channelId', isAuthenticated, closeChannel);

@ -1,11 +1,12 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; import { isAuthenticated } from '../../utils/authCheck.js';
import { getChannels, getChannelStats, openChannel, updateChannelRelayFee, closeChannel } from '../../controllers/eclair/channels.js'; import { getChannels, getChannelStats, openChannel, updateChannelRelayFee, closeChannel, circularRebalance } from '../../controllers/eclair/channels.js';
const router = Router(); const router = Router();
router.get('/', isAuthenticated, getChannels); router.get('/', isAuthenticated, getChannels);
router.get('/stats', isAuthenticated, getChannelStats); router.get('/stats', isAuthenticated, getChannelStats);
router.post('/', isAuthenticated, openChannel); router.post('/', isAuthenticated, openChannel);
router.post('/updateRelayFee', isAuthenticated, updateChannelRelayFee); router.post('/updateRelayFee', isAuthenticated, updateChannelRelayFee);
router.post('/circularRebalance', circularRebalance);
router.delete('/', isAuthenticated, closeChannel); router.delete('/', isAuthenticated, closeChannel);
export default router; export default router;

@ -1,7 +1,8 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; import { isAuthenticated } from '../../utils/authCheck.js';
import { getNodes } from '../../controllers/eclair/network.js'; import { getNodes, findRouteBetweenNodes } from '../../controllers/eclair/network.js';
const router = Router(); const router = Router();
router.get('/nodes/:id', isAuthenticated, getNodes); router.get('/nodes/:id', isAuthenticated, getNodes);
router.get('/routebetweennodes', isAuthenticated, findRouteBetweenNodes);
export default router; export default router;

@ -1,10 +1,11 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; import { isAuthenticated } from '../../utils/authCheck.js';
import { queryPaymentRoute, decodePayment, getSentPaymentsInformation, postPayment } from '../../controllers/eclair/payments.js'; import { queryPaymentRoute, decodePayment, getSentPaymentsInformation, postPayment, sendPaymentToRoute } from '../../controllers/eclair/payments.js';
const router = Router(); const router = Router();
router.get('/route/', isAuthenticated, queryPaymentRoute); router.get('/route/', isAuthenticated, queryPaymentRoute);
router.get('/decode/:invoice', isAuthenticated, decodePayment); router.get('/decode/:invoice', isAuthenticated, decodePayment);
router.post('/getsentinfos', isAuthenticated, getSentPaymentsInformation); router.post('/getsentinfos', isAuthenticated, getSentPaymentsInformation);
router.post('/sendtoroute', isAuthenticated, sendPaymentToRoute);
router.post('/', isAuthenticated, postPayment); router.post('/', isAuthenticated, postPayment);
export default router; export default router;

@ -12,7 +12,6 @@ import clnRoutes from '../routes/cln/index.js';
import eclRoutes from '../routes/eclair/index.js'; import eclRoutes from '../routes/eclair/index.js';
import { Common } from './common.js'; import { Common } from './common.js';
import { Logger } from './logger.js'; import { Logger } from './logger.js';
import { Config } from './config.js';
import { CLWSClient } from '../controllers/cln/webSocketClient.js'; import { CLWSClient } from '../controllers/cln/webSocketClient.js';
import { ECLWSClient } from '../controllers/eclair/webSocketClient.js'; import { ECLWSClient } from '../controllers/eclair/webSocketClient.js';
import { LNDWSClient } from '../controllers/lnd/webSocketClient.js'; import { LNDWSClient } from '../controllers/lnd/webSocketClient.js';
@ -22,15 +21,11 @@ export class ExpressApplication {
this.app = express(); this.app = express();
this.logger = Logger; this.logger = Logger;
this.common = Common; this.common = Common;
this.config = Config;
this.eclWsClient = ECLWSClient; this.eclWsClient = ECLWSClient;
this.clWsClient = CLWSClient; this.clWsClient = CLWSClient;
this.lndWsClient = LNDWSClient; this.lndWsClient = LNDWSClient;
this.directoryName = dirname(fileURLToPath(import.meta.url)); this.directoryName = dirname(fileURLToPath(import.meta.url));
this.getApp = () => this.app; this.getApp = () => this.app;
this.loadConfiguration = () => {
this.config.setServerConfiguration();
};
this.setCORS = () => { CORS.mount(this.app); }; this.setCORS = () => { CORS.mount(this.app); };
this.setCSRF = () => { CSRF.mount(this.app); }; this.setCSRF = () => { CSRF.mount(this.app); };
this.setApplicationRoutes = () => { this.setApplicationRoutes = () => {
@ -81,7 +76,6 @@ export class ExpressApplication {
this.app.use(cookieParser(this.common.secret_key)); this.app.use(cookieParser(this.common.secret_key));
this.app.use(bodyParser.json({ limit: '25mb' })); this.app.use(bodyParser.json({ limit: '25mb' }));
this.app.use(bodyParser.urlencoded({ extended: false, limit: '25mb' })); this.app.use(bodyParser.urlencoded({ extended: false, limit: '25mb' }));
this.loadConfiguration();
this.setCORS(); this.setCORS();
this.setCSRF(); this.setCSRF();
this.setApplicationRoutes(); this.setApplicationRoutes();

@ -11,7 +11,8 @@ export class CommonService {
this.initSelectedNode = null; this.initSelectedNode = null;
this.rtl_conf_file_path = ''; this.rtl_conf_file_path = '';
this.port = 3000; this.port = 3000;
this.host = null; this.host = '';
this.db_directory_path = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
this.rtl_pass = ''; this.rtl_pass = '';
this.flg_allow_password_update = true; this.flg_allow_password_update = true;
this.rtl_secret2fa = ''; this.rtl_secret2fa = '';
@ -219,7 +220,11 @@ export class CommonService {
} }
}; };
this.handleError = (errRes, fileName, errMsg, selectedNode) => { this.handleError = (errRes, fileName, errMsg, selectedNode) => {
const err = JSON.parse(JSON.stringify(errRes)); let err = JSON.parse(JSON.stringify(errRes));
if (err && err.error && Object.keys(err.error).length === 0 && errRes.error && (errRes.error.stack || errRes.error.message)) {
errRes.error = errRes.error.stack || errRes.error.message;
err = JSON.parse(JSON.stringify(errRes));
}
if (!selectedNode) { if (!selectedNode) {
selectedNode = this.initSelectedNode; selectedNode = this.initSelectedNode;
} }
@ -254,7 +259,7 @@ export class CommonService {
} }
break; break;
} }
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') }); this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : err) });
let newErrorObj = { statusCode: 500, message: '', error: '' }; let newErrorObj = { statusCode: 500, message: '', error: '' };
if (err.code && err.code === 'ENOENT') { if (err.code && err.code === 'ENOENT') {
newErrorObj = { newErrorObj = {
@ -316,7 +321,7 @@ export class CommonService {
this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8'); this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8');
} }
catch (err) { catch (err) {
this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while reading cookie: \n' + err }); this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Something went wrong while reading cookie: \n' + err });
throw new Error(err); throw new Error(err);
} }
} }
@ -328,7 +333,7 @@ export class CommonService {
this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8'); this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8');
} }
catch (err) { catch (err) {
this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while reading the cookie: \n' + err }); this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Something went wrong while reading the cookie: \n' + err });
throw new Error(err); throw new Error(err);
} }
} }
@ -429,6 +434,7 @@ export class CommonService {
if (selNode && selNode.index) { if (selNode && selNode.index) {
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'PORT: ' + this.port }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'PORT: ' + this.port });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'HOST: ' + this.host }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'HOST: ' + this.host });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'DB_DIRECTORY_PATH: ' + this.db_directory_path });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'SSO: ' + this.rtl_sso }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'SSO: ' + this.rtl_sso });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'DEFAULT NODE INDEX: ' + selNode.index }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'DEFAULT NODE INDEX: ' + selNode.index });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'INDEX: ' + selNode.index }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'INDEX: ' + selNode.index });

@ -20,31 +20,37 @@ export class ConfigService {
let macaroonPath = ''; let macaroonPath = '';
let configPath = ''; let configPath = '';
let channelBackupPath = ''; let channelBackupPath = '';
let dbPath = '';
switch (this.platform) { switch (this.platform) {
case 'win32': case 'win32':
macaroonPath = homeDir + '\\AppData\\Local\\Lnd\\data\\chain\\bitcoin\\mainnet'; macaroonPath = homeDir + '\\AppData\\Local\\Lnd\\data\\chain\\bitcoin\\mainnet';
configPath = homeDir + '\\AppData\\Local\\Lnd\\lnd.conf'; configPath = homeDir + '\\AppData\\Local\\Lnd\\lnd.conf';
channelBackupPath = homeDir + '\\backup\\node-1'; channelBackupPath = homeDir + '\\backup\\node-1';
dbPath = homeDir + '\\database\\node-1';
break; break;
case 'darwin': case 'darwin':
macaroonPath = homeDir + '/Library/Application Support/Lnd/data/chain/bitcoin/mainnet'; macaroonPath = homeDir + '/Library/Application Support/Lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/Library/Application Support/Lnd/lnd.conf'; configPath = homeDir + '/Library/Application Support/Lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1'; channelBackupPath = homeDir + '/backup/node-1';
dbPath = homeDir + '/database/node-1';
break; break;
case 'linux': case 'linux':
macaroonPath = homeDir + '/.lnd/data/chain/bitcoin/mainnet'; macaroonPath = homeDir + '/.lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/.lnd/lnd.conf'; configPath = homeDir + '/.lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1'; channelBackupPath = homeDir + '/backup/node-1';
dbPath = homeDir + '/database/node-1';
break; break;
default: default:
macaroonPath = ''; macaroonPath = '';
configPath = ''; configPath = '';
channelBackupPath = ''; channelBackupPath = '';
dbPath = '';
break; break;
} }
const configData = { const configData = {
port: '3000', port: '3000',
defaultNodeIndex: 1, defaultNodeIndex: 1,
dbDirectoryPath: dbPath,
SSO: { SSO: {
rtlSSO: 0, rtlSSO: 0,
rtlCookiePath: '', rtlCookiePath: '',
@ -72,7 +78,7 @@ export class ConfigService {
} }
] ]
}; };
if (+process.env.RTL_SSO === 0 || configData.SSO.rtlSSO === 0) { if ((process?.env?.RTL_SSO && +process?.env?.RTL_SSO === 0) || configData.SSO.rtlSSO === 0) {
configData['multiPass'] = 'password'; configData['multiPass'] = 'password';
} }
return configData; return configData;
@ -89,7 +95,7 @@ export class ConfigService {
}; };
this.updateLogByLevel = () => { this.updateLogByLevel = () => {
let updateLogFlag = false; let updateLogFlag = false;
this.common.rtl_conf_file_path = process.env.RTL_CONFIG_PATH ? process.env.RTL_CONFIG_PATH : join(this.directoryName, '../..'); this.common.rtl_conf_file_path = process?.env?.RTL_CONFIG_PATH ? process?.env?.RTL_CONFIG_PATH : join(this.directoryName, '../..');
try { try {
const RTLConfFile = this.common.rtl_conf_file_path + sep + 'RTL-Config.json'; const RTLConfFile = this.common.rtl_conf_file_path + sep + 'RTL-Config.json';
const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8'));
@ -109,9 +115,9 @@ export class ConfigService {
} }
}; };
this.validateNodeConfig = (config) => { this.validateNodeConfig = (config) => {
if ((+process.env.RTL_SSO === 0) || (typeof process.env.RTL_SSO === 'undefined' && +config.SSO.rtlSSO === 0)) { if ((process?.env?.RTL_SSO && +process?.env?.RTL_SSO === 0) || (typeof process?.env?.RTL_SSO === 'undefined' && +config.SSO.rtlSSO === 0)) {
if (process.env.APP_PASSWORD && process.env.APP_PASSWORD.trim() !== '') { if (process?.env?.APP_PASSWORD && process?.env?.APP_PASSWORD.trim() !== '') {
this.common.rtl_pass = this.hash.update(process.env.APP_PASSWORD).digest('hex'); this.common.rtl_pass = this.hash.update(process?.env?.APP_PASSWORD).digest('hex');
this.common.flg_allow_password_update = false; this.common.flg_allow_password_update = false;
} }
else if (config.multiPassHashed && config.multiPassHashed !== '') { else if (config.multiPassHashed && config.multiPassHashed !== '') {
@ -126,23 +132,24 @@ export class ConfigService {
this.common.rtl_secret2fa = config.secret2fa; this.common.rtl_secret2fa = config.secret2fa;
} }
else { else {
if (process.env.APP_PASSWORD && process.env.APP_PASSWORD.trim() !== '') { if (process?.env?.APP_PASSWORD && process?.env?.APP_PASSWORD.trim() !== '') {
this.errMsg = this.errMsg + '\nRTL Password cannot be set with SSO. Please set SSO as 0 or remove password.'; this.errMsg = this.errMsg + '\nRTL Password cannot be set with SSO. Please set SSO as 0 or remove password.';
} }
} }
this.common.port = (process.env.PORT) ? this.normalizePort(process.env.PORT) : (config.port) ? this.normalizePort(config.port) : 3000; this.common.port = (process?.env?.PORT) ? this.normalizePort(process?.env?.PORT) : (config.port) ? this.normalizePort(config.port) : 3000;
this.common.host = (process.env.HOST) ? process.env.HOST : (config.host) ? config.host : null; this.common.host = (process?.env?.HOST) ? process?.env?.HOST : (config.host) ? config.host : null;
this.common.db_directory_path = (process?.env?.DB_DIRECTORY_PATH) ? process?.env?.DB_DIRECTORY_PATH : (config.dbDirectoryPath) ? config.dbDirectoryPath : join(dirname(fileURLToPath(import.meta.url)), '..', '..');
if (config.nodes && config.nodes.length > 0) { if (config.nodes && config.nodes.length > 0) {
config.nodes.forEach((node, idx) => { config.nodes.forEach((node, idx) => {
this.common.nodes[idx] = {}; this.common.nodes[idx] = {};
this.common.nodes[idx].index = node.index; this.common.nodes[idx].index = node.index;
this.common.nodes[idx].ln_node = node.lnNode; this.common.nodes[idx].ln_node = node.lnNode;
this.common.nodes[idx].ln_implementation = (process.env.LN_IMPLEMENTATION) ? process.env.LN_IMPLEMENTATION : node.lnImplementation ? node.lnImplementation : 'LND'; this.common.nodes[idx].ln_implementation = (process?.env?.LN_IMPLEMENTATION) ? process?.env?.LN_IMPLEMENTATION : node.lnImplementation ? node.lnImplementation : 'LND';
if (this.common.nodes[idx].ln_implementation === 'CLT') { if (this.common.nodes[idx].ln_implementation === 'CLT') {
this.common.nodes[idx].ln_implementation = 'CLN'; this.common.nodes[idx].ln_implementation = 'CLN';
} }
if (this.common.nodes[idx].ln_implementation !== 'ECL' && process.env.MACAROON_PATH && process.env.MACAROON_PATH.trim() !== '') { if (this.common.nodes[idx].ln_implementation !== 'ECL' && process?.env?.MACAROON_PATH && process?.env?.MACAROON_PATH.trim() !== '') {
this.common.nodes[idx].macaroon_path = process.env.MACAROON_PATH; this.common.nodes[idx].macaroon_path = process?.env?.MACAROON_PATH;
} }
else if (this.common.nodes[idx].ln_implementation !== 'ECL' && node.Authentication && node.Authentication.macaroonPath && node.Authentication.macaroonPath.trim() !== '') { else if (this.common.nodes[idx].ln_implementation !== 'ECL' && node.Authentication && node.Authentication.macaroonPath && node.Authentication.macaroonPath.trim() !== '') {
this.common.nodes[idx].macaroon_path = node.Authentication.macaroonPath; this.common.nodes[idx].macaroon_path = node.Authentication.macaroonPath;
@ -151,8 +158,8 @@ export class ConfigService {
this.errMsg = 'Please set macaroon path for node index ' + node.index + ' in RTL-Config.json!'; this.errMsg = 'Please set macaroon path for node index ' + node.index + ' in RTL-Config.json!';
} }
if (this.common.nodes[idx].ln_implementation === 'ECL') { if (this.common.nodes[idx].ln_implementation === 'ECL') {
if (process.env.LN_API_PASSWORD) { if (process?.env?.LN_API_PASSWORD) {
this.common.nodes[idx].ln_api_password = process.env.LN_API_PASSWORD; this.common.nodes[idx].ln_api_password = process?.env?.LN_API_PASSWORD;
} }
else if (node.Authentication && node.Authentication.lnApiPassword) { else if (node.Authentication && node.Authentication.lnApiPassword) {
this.common.nodes[idx].ln_api_password = node.Authentication.lnApiPassword; this.common.nodes[idx].ln_api_password = node.Authentication.lnApiPassword;
@ -161,8 +168,8 @@ export class ConfigService {
this.common.nodes[idx].ln_api_password = ''; this.common.nodes[idx].ln_api_password = '';
} }
} }
if (process.env.CONFIG_PATH) { if (process?.env?.CONFIG_PATH) {
this.common.nodes[idx].config_path = process.env.CONFIG_PATH; this.common.nodes[idx].config_path = process?.env?.CONFIG_PATH;
} }
else if (node.Authentication && node.Authentication.configPath) { else if (node.Authentication && node.Authentication.configPath) {
this.common.nodes[idx].config_path = node.Authentication.configPath; this.common.nodes[idx].config_path = node.Authentication.configPath;
@ -172,10 +179,10 @@ export class ConfigService {
} }
if (this.common.nodes[idx].ln_implementation === 'ECL' && this.common.nodes[idx].ln_api_password === '' && this.common.nodes[idx].config_path !== '') { if (this.common.nodes[idx].ln_implementation === 'ECL' && this.common.nodes[idx].ln_api_password === '' && this.common.nodes[idx].config_path !== '') {
try { try {
const exists = fs.existsSync(this.common.nodes[idx].config_path); const exists = fs.existsSync(this.common.nodes[idx].config_path || '');
if (exists) { if (exists) {
try { try {
const configFile = fs.readFileSync(this.common.nodes[idx].config_path, 'utf-8'); const configFile = fs.readFileSync((this.common.nodes[idx].config_path || ''), 'utf-8');
const iniParsed = ini.parse(configFile); const iniParsed = ini.parse(configFile);
this.common.nodes[idx].ln_api_password = iniParsed['eclair.api.password'] ? iniParsed['eclair.api.password'] : parseHocon(configFile).eclair.api.password; this.common.nodes[idx].ln_api_password = iniParsed['eclair.api.password'] ? iniParsed['eclair.api.password'] : parseHocon(configFile).eclair.api.password;
} }
@ -194,11 +201,11 @@ export class ConfigService {
if (this.common.nodes[idx].ln_implementation === 'ECL' && this.common.nodes[idx].ln_api_password === '') { if (this.common.nodes[idx].ln_implementation === 'ECL' && this.common.nodes[idx].ln_api_password === '') {
this.errMsg = this.errMsg + '\nPlease set config path Or api password for node index ' + node.index + ' in RTL-Config.json! It is mandatory for Eclair authentication!'; this.errMsg = this.errMsg + '\nPlease set config path Or api password for node index ' + node.index + ' in RTL-Config.json! It is mandatory for Eclair authentication!';
} }
if (process.env.LN_SERVER_URL && process.env.LN_SERVER_URL.trim() !== '') { if (process?.env?.LN_SERVER_URL && process?.env?.LN_SERVER_URL.trim() !== '') {
this.common.nodes[idx].ln_server_url = process.env.LN_SERVER_URL.endsWith('/v1') ? process.env.LN_SERVER_URL.slice(0, -3) : process.env.LN_SERVER_URL; this.common.nodes[idx].ln_server_url = process?.env?.LN_SERVER_URL.endsWith('/v1') ? process?.env?.LN_SERVER_URL.slice(0, -3) : process?.env?.LN_SERVER_URL;
} }
else if (process.env.LND_SERVER_URL && process.env.LND_SERVER_URL.trim() !== '') { else if (process?.env?.LND_SERVER_URL && process?.env?.LND_SERVER_URL.trim() !== '') {
this.common.nodes[idx].ln_server_url = process.env.LND_SERVER_URL.endsWith('/v1') ? process.env.LND_SERVER_URL.slice(0, -3) : process.env.LND_SERVER_URL; this.common.nodes[idx].ln_server_url = process?.env?.LND_SERVER_URL.endsWith('/v1') ? process?.env?.LND_SERVER_URL.slice(0, -3) : process?.env?.LND_SERVER_URL;
} }
else if (node.Settings.lnServerUrl && node.Settings.lnServerUrl.trim() !== '') { else if (node.Settings.lnServerUrl && node.Settings.lnServerUrl.trim() !== '') {
this.common.nodes[idx].ln_server_url = node.Settings.lnServerUrl.endsWith('/v1') ? node.Settings.lnServerUrl.slice(0, -3) : node.Settings.lnServerUrl; this.common.nodes[idx].ln_server_url = node.Settings.lnServerUrl.endsWith('/v1') ? node.Settings.lnServerUrl.slice(0, -3) : node.Settings.lnServerUrl;
@ -218,9 +225,9 @@ export class ConfigService {
if (this.common.nodes[idx].fiat_conversion) { if (this.common.nodes[idx].fiat_conversion) {
this.common.nodes[idx].currency_unit = node.Settings.currencyUnit ? node.Settings.currencyUnit : 'USD'; this.common.nodes[idx].currency_unit = node.Settings.currencyUnit ? node.Settings.currencyUnit : 'USD';
} }
if (process.env.SWAP_SERVER_URL && process.env.SWAP_SERVER_URL.trim() !== '') { if (process?.env?.SWAP_SERVER_URL && process?.env?.SWAP_SERVER_URL.trim() !== '') {
this.common.nodes[idx].swap_server_url = process.env.SWAP_SERVER_URL.endsWith('/v1') ? process.env.SWAP_SERVER_URL.slice(0, -3) : process.env.SWAP_SERVER_URL; this.common.nodes[idx].swap_server_url = process?.env?.SWAP_SERVER_URL.endsWith('/v1') ? process?.env?.SWAP_SERVER_URL.slice(0, -3) : process?.env?.SWAP_SERVER_URL;
this.common.nodes[idx].swap_macaroon_path = process.env.SWAP_MACAROON_PATH; this.common.nodes[idx].swap_macaroon_path = process?.env?.SWAP_MACAROON_PATH;
} }
else if (node.Settings.swapServerUrl && node.Settings.swapServerUrl.trim() !== '') { else if (node.Settings.swapServerUrl && node.Settings.swapServerUrl.trim() !== '') {
this.common.nodes[idx].swap_server_url = node.Settings.swapServerUrl.endsWith('/v1') ? node.Settings.swapServerUrl.slice(0, -3) : node.Settings.swapServerUrl; this.common.nodes[idx].swap_server_url = node.Settings.swapServerUrl.endsWith('/v1') ? node.Settings.swapServerUrl.slice(0, -3) : node.Settings.swapServerUrl;
@ -230,9 +237,9 @@ export class ConfigService {
this.common.nodes[idx].swap_server_url = ''; this.common.nodes[idx].swap_server_url = '';
this.common.nodes[idx].swap_macaroon_path = ''; this.common.nodes[idx].swap_macaroon_path = '';
} }
if (process.env.BOLTZ_SERVER_URL && process.env.BOLTZ_SERVER_URL.trim() !== '') { if (process?.env?.BOLTZ_SERVER_URL && process?.env?.BOLTZ_SERVER_URL.trim() !== '') {
this.common.nodes[idx].boltz_server_url = process.env.BOLTZ_SERVER_URL.endsWith('/v1') ? process.env.BOLTZ_SERVER_URL.slice(0, -3) : process.env.BOLTZ_SERVER_URL; this.common.nodes[idx].boltz_server_url = process?.env?.BOLTZ_SERVER_URL.endsWith('/v1') ? process?.env?.BOLTZ_SERVER_URL.slice(0, -3) : process?.env?.BOLTZ_SERVER_URL;
this.common.nodes[idx].boltz_macaroon_path = process.env.BOLTZ_MACAROON_PATH; this.common.nodes[idx].boltz_macaroon_path = process?.env?.BOLTZ_MACAROON_PATH;
} }
else if (node.Settings.boltzServerUrl && node.Settings.boltzServerUrl.trim() !== '') { else if (node.Settings.boltzServerUrl && node.Settings.boltzServerUrl.trim() !== '') {
this.common.nodes[idx].boltz_server_url = node.Settings.boltzServerUrl.endsWith('/v1') ? node.Settings.boltzServerUrl.slice(0, -3) : node.Settings.boltzServerUrl; this.common.nodes[idx].boltz_server_url = node.Settings.boltzServerUrl.endsWith('/v1') ? node.Settings.boltzServerUrl.slice(0, -3) : node.Settings.boltzServerUrl;
@ -242,10 +249,10 @@ export class ConfigService {
this.common.nodes[idx].boltz_server_url = ''; this.common.nodes[idx].boltz_server_url = '';
this.common.nodes[idx].boltz_macaroon_path = ''; this.common.nodes[idx].boltz_macaroon_path = '';
} }
this.common.nodes[idx].enable_offers = process.env.ENABLE_OFFERS ? process.env.ENABLE_OFFERS : (node.Settings.enableOffers) ? node.Settings.enableOffers : false; this.common.nodes[idx].enable_offers = process?.env?.ENABLE_OFFERS ? process?.env?.ENABLE_OFFERS : (node.Settings.enableOffers) ? node.Settings.enableOffers : false;
this.common.nodes[idx].enable_peerswap = process.env.ENABLE_PEERSWAP ? process.env.ENABLE_PEERSWAP : (node.Settings.enablePeerswap) ? node.Settings.enablePeerswap : false; this.common.nodes[idx].enable_peerswap = process?.env?.ENABLE_PEERSWAP ? process?.env?.ENABLE_PEERSWAP : (node.Settings.enablePeerswap) ? node.Settings.enablePeerswap : false;
this.common.nodes[idx].bitcoind_config_path = process.env.BITCOIND_CONFIG_PATH ? process.env.BITCOIND_CONFIG_PATH : (node.Settings.bitcoindConfigPath) ? node.Settings.bitcoindConfigPath : ''; this.common.nodes[idx].bitcoind_config_path = process?.env?.BITCOIND_CONFIG_PATH ? process?.env?.BITCOIND_CONFIG_PATH : (node.Settings.bitcoindConfigPath) ? node.Settings.bitcoindConfigPath : '';
this.common.nodes[idx].channel_backup_path = process.env.CHANNEL_BACKUP_PATH ? process.env.CHANNEL_BACKUP_PATH : (node.Settings.channelBackupPath) ? node.Settings.channelBackupPath : this.common.rtl_conf_file_path + sep + 'channels-backup' + sep + 'node-' + node.index; this.common.nodes[idx].channel_backup_path = process?.env?.CHANNEL_BACKUP_PATH ? process?.env?.CHANNEL_BACKUP_PATH : (node.Settings.channelBackupPath) ? node.Settings.channelBackupPath : this.common.rtl_conf_file_path + sep + 'channels-backup' + sep + 'node-' + node.index;
try { try {
this.common.createDirectory(this.common.nodes[idx].channel_backup_path); this.common.createDirectory(this.common.nodes[idx].channel_backup_path);
const exists = fs.existsSync(this.common.nodes[idx].channel_backup_path + sep + 'channel-all.bak'); const exists = fs.existsSync(this.common.nodes[idx].channel_backup_path + sep + 'channel-all.bak');
@ -270,14 +277,14 @@ export class ConfigService {
this.common.nodes[idx].log_file = this.common.rtl_conf_file_path + '/logs/RTL-Node-' + node.index + '.log'; this.common.nodes[idx].log_file = this.common.rtl_conf_file_path + '/logs/RTL-Node-' + node.index + '.log';
this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'Config', msg: 'Node Config: ' + JSON.stringify(this.common.nodes[idx]) }); this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'Config', msg: 'Node Config: ' + JSON.stringify(this.common.nodes[idx]) });
const log_file = this.common.nodes[idx].log_file; const log_file = this.common.nodes[idx].log_file;
if (fs.existsSync(log_file)) { if (fs.existsSync(log_file || '')) {
fs.writeFile(log_file, '', () => { }); fs.writeFile((log_file || ''), '', () => { });
} }
else { else {
try { try {
const directoryName = dirname(log_file); const directoryName = dirname(log_file || '');
this.common.createDirectory(directoryName); this.common.createDirectory(directoryName);
const createStream = fs.createWriteStream(log_file); const createStream = fs.createWriteStream(log_file || '');
createStream.end(); createStream.end();
} }
catch (err) { catch (err) {
@ -292,14 +299,14 @@ export class ConfigService {
} }
}; };
this.setSSOParams = (config) => { this.setSSOParams = (config) => {
if (process.env.RTL_SSO) { if (process?.env?.RTL_SSO) {
this.common.rtl_sso = +process.env.RTL_SSO; this.common.rtl_sso = +process?.env?.RTL_SSO;
} }
else if (config.SSO && config.SSO.rtlSSO) { else if (config.SSO && config.SSO.rtlSSO) {
this.common.rtl_sso = config.SSO.rtlSSO; this.common.rtl_sso = config.SSO.rtlSSO;
} }
if (process.env.RTL_COOKIE_PATH) { if (process?.env?.RTL_COOKIE_PATH) {
this.common.rtl_cookie_path = process.env.RTL_COOKIE_PATH; this.common.rtl_cookie_path = process?.env?.RTL_COOKIE_PATH;
} }
else if (config.SSO && config.SSO.rtlCookiePath) { else if (config.SSO && config.SSO.rtlCookiePath) {
this.common.rtl_cookie_path = config.SSO.rtlCookiePath; this.common.rtl_cookie_path = config.SSO.rtlCookiePath;
@ -307,8 +314,8 @@ export class ConfigService {
else { else {
this.common.rtl_cookie_path = ''; this.common.rtl_cookie_path = '';
} }
if (process.env.LOGOUT_REDIRECT_LINK) { if (process?.env?.LOGOUT_REDIRECT_LINK) {
this.common.logout_redirect_link = process.env.LOGOUT_REDIRECT_LINK; this.common.logout_redirect_link = process?.env?.LOGOUT_REDIRECT_LINK;
} }
else if (config.SSO && config.SSO.logoutRedirectLink) { else if (config.SSO && config.SSO.logoutRedirectLink) {
this.common.logout_redirect_link = config.SSO.logoutRedirectLink; this.common.logout_redirect_link = config.SSO.logoutRedirectLink;
@ -324,15 +331,15 @@ export class ConfigService {
}; };
this.setSelectedNode = (config) => { this.setSelectedNode = (config) => {
if (config.defaultNodeIndex) { if (config.defaultNodeIndex) {
this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex); this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex) || {};
} }
else { else {
this.common.initSelectedNode = this.common.findNode(this.common.nodes[0].index); this.common.initSelectedNode = this.common.findNode(this.common.nodes[0].index) || {};
} }
}; };
this.setServerConfiguration = () => { this.setServerConfiguration = () => {
try { try {
this.common.rtl_conf_file_path = (process.env.RTL_CONFIG_PATH) ? process.env.RTL_CONFIG_PATH : join(this.directoryName, '../..'); this.common.rtl_conf_file_path = (process?.env?.RTL_CONFIG_PATH) ? process?.env?.RTL_CONFIG_PATH : join(this.directoryName, '../..');
const confFileFullPath = this.common.rtl_conf_file_path + sep + 'RTL-Config.json'; const confFileFullPath = this.common.rtl_conf_file_path + sep + 'RTL-Config.json';
if (!fs.existsSync(confFileFullPath)) { if (!fs.existsSync(confFileFullPath)) {
fs.writeFileSync(confFileFullPath, JSON.stringify(this.setDefaultConfig())); fs.writeFileSync(confFileFullPath, JSON.stringify(this.setDefaultConfig()));
@ -347,6 +354,7 @@ export class ConfigService {
throw new Error(err); throw new Error(err);
} }
}; };
this.setServerConfiguration();
} }
} }
export const Config = new ConfigService(); export const Config = new ConfigService();

@ -1,6 +1,5 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { join, dirname, sep } from 'path'; import { join, sep } from 'path';
import { fileURLToPath } from 'url';
import { Common } from '../utils/common.js'; import { Common } from '../utils/common.js';
import { Logger } from '../utils/logger.js'; import { Logger } from '../utils/logger.js';
import { validateDocument, LNDCollection, ECLCollection, CLNCollection } from '../models/database.model.js'; import { validateDocument, LNDCollection, ECLCollection, CLNCollection } from '../models/database.model.js';
@ -8,7 +7,7 @@ export class DatabaseService {
constructor() { constructor() {
this.common = Common; this.common = Common;
this.logger = Logger; this.logger = Logger;
this.dbDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'database'); this.dbDirectory = join(this.common.db_directory_path, 'database');
this.nodeDatabase = {}; this.nodeDatabase = {};
} }
loadDatabase(session) { loadDatabase(session) {

@ -8,7 +8,7 @@ WORKDIR /RTL
COPY package.json /RTL/package.json COPY package.json /RTL/package.json
COPY package-lock.json /RTL/package-lock.json COPY package-lock.json /RTL/package-lock.json
RUN npm install RUN npm install --legacy-peer-deps
# --------------- # ---------------
# Build App # Build App
@ -22,7 +22,7 @@ RUN npm run buildfrontend
RUN npm run buildbackend RUN npm run buildbackend
# Remove non production necessary modules # Remove non production necessary modules
RUN npm prune --production RUN npm prune --production --legacy-peer-deps
# --------------- # ---------------
# Release App # Release App

@ -12,7 +12,7 @@ WORKDIR /RTL
COPY package.json /RTL/package.json COPY package.json /RTL/package.json
COPY package-lock.json /RTL/package-lock.json COPY package-lock.json /RTL/package-lock.json
RUN npm install RUN npm install --legacy-peer-deps
# --------------- # ---------------
# Build App # Build App
@ -26,7 +26,7 @@ RUN npm run buildfrontend
RUN npm run buildbackend RUN npm run buildbackend
# Remove non production necessary modules # Remove non production necessary modules
RUN npm prune --production RUN npm prune --production --legacy-peer-deps
# --------------- # ---------------
# Release App # Release App

@ -11,7 +11,7 @@ WORKDIR /RTL
COPY package.json /RTL/package.json COPY package.json /RTL/package.json
COPY package-lock.json /RTL/package-lock.json COPY package-lock.json /RTL/package-lock.json
RUN npm install RUN npm install --legacy-peer-deps
# --------------- # ---------------
# Build App # Build App
@ -25,7 +25,7 @@ RUN npm run buildfrontend
RUN npm run buildbackend RUN npm run buildbackend
# Remove non production necessary modules # Remove non production necessary modules
RUN npm prune --production RUN npm prune --production --legacy-peer-deps
# --------------- # ---------------
# Release App # Release App

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

File diff suppressed because one or more lines are too long

@ -5,7 +5,7 @@ MIT
MIT MIT
The MIT License The MIT License
Copyright (c) 2022 Google LLC. Copyright (c) 2023 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -42,7 +42,7 @@ MIT
MIT MIT
The MIT License The MIT License
Copyright (c) 2022 Google LLC. Copyright (c) 2023 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -121,7 +121,7 @@ as SVG and JS file types.
In the Font Awesome Free download, the SIL OFL license applies to all icons In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files. packaged as web and desktop font files.
Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com) Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome". with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
@ -221,7 +221,7 @@ OTHER DEALINGS IN THE FONT SOFTWARE.
In the Font Awesome Free download, the MIT license applies to all non-font and In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files. non-icon files.
Copyright 2022 Fonticons, Inc. Copyright 2023 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the this software and associated documentation files (the "Software"), to deal in the
@ -290,7 +290,7 @@ as SVG and JS file types.
In the Font Awesome Free download, the SIL OFL license applies to all icons In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files. packaged as web and desktop font files.
Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com) Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome". with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
@ -390,7 +390,7 @@ OTHER DEALINGS IN THE FONT SOFTWARE.
In the Font Awesome Free download, the MIT license applies to all non-font and In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files. non-icon files.
Copyright 2022 Fonticons, Inc. Copyright 2023 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the this software and associated documentation files (the "Software"), to deal in the
@ -459,7 +459,7 @@ as SVG and JS file types.
In the Font Awesome Free download, the SIL OFL license applies to all icons In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files. packaged as web and desktop font files.
Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com) Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome". with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
@ -559,7 +559,7 @@ OTHER DEALINGS IN THE FONT SOFTWARE.
In the Font Awesome Free download, the MIT license applies to all non-font and In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files. non-icon files.
Copyright 2022 Fonticons, Inc. Copyright 2023 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the this software and associated documentation files (the "Software"), to deal in the
@ -601,31 +601,6 @@ Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.** to represent the company, product, or service to which they refer.**
@material/dialog
MIT
The MIT License
Copyright (c) 2014-2020 Google, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@ngrx/effects @ngrx/effects
MIT MIT

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

@ -1,19 +1,18 @@
<!DOCTYPE html><html lang="en"><head> <!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>RTL</title> <title>RTL</title>
<base href="/rtl/"> <base href="/rtl/">
<meta i18n-content="" name="viewport" content="width=device-width, initial-scale=1"> <meta i18n-content="" name="viewport" content="width=device-width, initial-scale=1">
<link i18n-sizes="" i18n-rel="" rel="apple-touch-icon" sizes="180x180" href="assets/images/favicon-light/apple-touch-icon.png"> <link i18n-sizes="" i18n-rel="" rel="apple-touch-icon" sizes="180x180" href="assets/images/favicon-light/apple-touch-icon.png">
<link i18n-sizes="" i18n-rel="" rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon-light/favicon-32x32.png"> <link i18n-sizes="" i18n-rel="" rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon-light/favicon-32x32.png">
<link i18n-sizes="" i18n-rel="" rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-light/favicon-16x16.png"> <link i18n-sizes="" i18n-rel="" rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-light/favicon-16x16.png">
<link i18n-rel="" rel="manifest" href="assets/images/favicon-light/site.webmanifest"> <link i18n-rel="" rel="manifest" href="assets/images/favicon-light/site.webmanifest">
<link i18n-rel="" rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5"> <link i18n-rel="" rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5">
<meta i18n-content="" name="msapplication-TileColor" content="#da532c"> <meta i18n-content="" name="msapplication-TileColor" content="#da532c">
<meta i18n-content="" name="theme-color" content="#ffffff"> <meta i18n-content="" name="theme-color" content="#ffffff">
<style>html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:100%}@media only screen and (max-width: 56.25em){html{font-size:90%}}@media only screen and (max-width: 37.5em){html{font-size:80%}}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}@font-face{font-family:Roboto;src:url(Roboto-Thin.f7a95c9c5999532c.woff2) format("woff2"),url(Roboto-Thin.c13c157cb81e8ebb.woff) format("woff");font-weight:100;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.b0e084abf689f393.woff2) format("woff2"),url(Roboto-ThinItalic.1111028df6cea564.woff) format("woff");font-weight:100;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Light.0e01b6cd13b3857f.woff2) format("woff2"),url(Roboto-Light.603ca9a537b88428.woff) format("woff");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.232ef4b20215f720.woff2) format("woff2"),url(Roboto-LightItalic.1b5e142f787151c8.woff) format("woff");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Regular.475ba9e4e2d63456.woff2) format("woff2"),url(Roboto-Regular.bcefbfee882bc1cb.woff) format("woff");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.e3a9ebdaac06bbc4.woff2) format("woff2"),url(Roboto-RegularItalic.0668fae6af0cf8c2.woff) format("woff");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Medium.457532032ceb0168.woff2) format("woff2"),url(Roboto-Medium.6e1ae5f0b324a0aa.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.872f7060602d55d2.woff2) format("woff2"),url(Roboto-MediumItalic.e06fb533801cbb08.woff) format("woff");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Bold.447291a88c067396.woff2) format("woff2"),url(Roboto-Bold.fc482e6133cf5e26.woff) format("woff");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.1b15168ef6fa4e16.woff2) format("woff2"),url(Roboto-BoldItalic.e26ba339b06f09f7.woff) format("woff");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Black.2eaa390d458c877d.woff2) format("woff2"),url(Roboto-Black.b25f67ad8583da68.woff) format("woff");font-weight:900;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.7dc03ee444552bc5.woff2) format("woff2"),url(Roboto-BlackItalic.c8dc642467cb3099.woff) format("woff");font-weight:900;font-style:italic}</style><link rel="stylesheet" href="styles.3f91aa9da8de48ca.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.3f91aa9da8de48ca.css"></noscript></head> <style>html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:95%}@media only screen and (max-width: 56.25em){html{font-size:90%}}@media only screen and (max-width: 37.5em){html{font-size:80%}}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}@font-face{font-family:Roboto;src:url(Roboto-Thin.f7a95c9c5999532c.woff2) format("woff2"),url(Roboto-Thin.c13c157cb81e8ebb.woff) format("woff");font-weight:100;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.b0e084abf689f393.woff2) format("woff2"),url(Roboto-ThinItalic.1111028df6cea564.woff) format("woff");font-weight:100;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Light.0e01b6cd13b3857f.woff2) format("woff2"),url(Roboto-Light.603ca9a537b88428.woff) format("woff");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.232ef4b20215f720.woff2) format("woff2"),url(Roboto-LightItalic.1b5e142f787151c8.woff) format("woff");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Regular.475ba9e4e2d63456.woff2) format("woff2"),url(Roboto-Regular.bcefbfee882bc1cb.woff) format("woff");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.e3a9ebdaac06bbc4.woff2) format("woff2"),url(Roboto-RegularItalic.0668fae6af0cf8c2.woff) format("woff");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Medium.457532032ceb0168.woff2) format("woff2"),url(Roboto-Medium.6e1ae5f0b324a0aa.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.872f7060602d55d2.woff2) format("woff2"),url(Roboto-MediumItalic.e06fb533801cbb08.woff) format("woff");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Bold.447291a88c067396.woff2) format("woff2"),url(Roboto-Bold.fc482e6133cf5e26.woff) format("woff");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.1b15168ef6fa4e16.woff2) format("woff2"),url(Roboto-BoldItalic.e26ba339b06f09f7.woff) format("woff");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Black.2eaa390d458c877d.woff2) format("woff2"),url(Roboto-Black.b25f67ad8583da68.woff) format("woff");font-weight:900;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.7dc03ee444552bc5.woff2) format("woff2"),url(Roboto-BlackItalic.c8dc642467cb3099.woff) format("woff");font-weight:900;font-style:italic}</style><link rel="stylesheet" href="styles.a04c018645a5044a.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.a04c018645a5044a.css"></noscript></head>
<body> <body>
<rtl-app></rtl-app> <rtl-app></rtl-app>
<script src="runtime.5ce8adc19f44fff4.js" type="module"></script><script src="polyfills.08e0233279c8a187.js" type="module"></script><script src="main.5a72aaa7ca7fb8a9.js" type="module"></script> <script src="runtime.2136ac2986261ec4.js" type="module"></script><script src="polyfills.aa01d8f6b94657cb.js" type="module"></script><script src="main.3ccfe42677016a42.js" type="module"></script>
<script>window.global = window;</script>
</body></html> </body></html>

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

@ -0,0 +1 @@
(()=>{"use strict";var e,v={},m={};function r(e){var o=m[e];if(void 0!==o)return o.exports;var t=m[e]={id:e,loaded:!1,exports:{}};return v[e].call(t.exports,t,t.exports,r),t.loaded=!0,t.exports}r.m=v,e=[],r.O=(o,t,i,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,f]=e[n],s=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(s=!1,f<a&&(a=f));if(s){e.splice(n--,1);var d=i();void 0!==d&&(o=d)}}return o}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,i,f]},r.d=(e,o)=>{for(var t in o)r.o(o,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((o,t)=>(r.f[t](e,o),o),[])),r.u=e=>e+"."+{167:"bba0ab48235f9054",267:"5508f97536cb5708",315:"d20113f8d2f54786",636:"8e6af702364d1f11"}[e]+".js",r.miniCssF=e=>{},r.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),(()=>{var e={},o="RTLApp:";r.l=(t,i,f,n)=>{if(e[t])e[t].push(i);else{var a,s;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==o+f){a=u;break}}a||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",o+f),a.src=r.tu(t)),e[t]=[i];var c=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(y=>y(b)),g)return g(b)},p=setTimeout(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:o=>o},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(i,f)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=i){var a=new Promise((u,c)=>n=e[i]=[u,c]);f.push(n[2]=a);var s=r.p+r.u(i),l=new Error;r.l(s,u=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var c=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+i+" failed.\n("+c+": "+p+")",l.name="ChunkLoadError",l.type=c,l.request=p,n[1](l)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var o=(i,f)=>{var l,d,[n,a,s]=f,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(s)var c=s(r)}for(i&&i(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[d]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))})()})();

@ -1 +0,0 @@
(()=>{"use strict";var e,v={},m={};function r(e){var f=m[e];if(void 0!==f)return f.exports;var t=m[e]={id:e,loaded:!1,exports:{}};return v[e].call(t.exports,t,t.exports,r),t.loaded=!0,t.exports}r.m=v,e=[],r.O=(f,t,i,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,o]=e[n],s=!0,d=0;d<t.length;d++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[d]))?t.splice(d--,1):(s=!1,o<a&&(a=o));if(s){e.splice(n--,1);var u=i();void 0!==u&&(f=u)}}return f}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,i,o]},r.d=(e,f)=>{for(var t in f)r.o(f,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:f[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((f,t)=>(r.f[t](e,f),f),[])),r.u=e=>e+"."+{258:"f20f2eadd74bc704",267:"6c439858ec5d06c8",564:"dd8d5bd71c0f2cf9",636:"cb7b120de491d4ef"}[e]+".js",r.miniCssF=e=>{},r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),(()=>{var e={},f="RTLApp:";r.l=(t,i,o,n)=>{if(e[t])e[t].push(i);else{var a,s;if(void 0!==o)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var l=d[u];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==f+o){a=l;break}}a||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",f+o),a.src=r.tu(t)),e[t]=[i];var c=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(y=>y(b)),g)return g(b)},p=setTimeout(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:f=>f},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(i,o)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=i){var a=new Promise((l,c)=>n=e[i]=[l,c]);o.push(n[2]=a);var s=r.p+r.u(i),d=new Error;r.l(s,l=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var c=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;d.message="Loading chunk "+i+" failed.\n("+c+": "+p+")",d.name="ChunkLoadError",d.type=c,d.request=p,n[1](d)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var f=(i,o)=>{var d,u,[n,a,s]=o,l=0;if(n.some(p=>0!==e[p])){for(d in a)r.o(a,d)&&(r.m[d]=a[d]);if(s)var c=s(r)}for(i&&i(o);l<n.length;l++)r.o(e,u=n[l])&&e[u]&&e[u][0](),e[u]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(f.bind(null,0)),t.push=f.bind(null,t.push.bind(t))})()})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

23683
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "rtl", "name": "rtl",
"version": "0.13.3-beta", "version": "0.14.0-beta",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -15,20 +15,12 @@
"server": "set NODE_ENV=development&&nodemon --watch backend --watch server ./rtl.js", "server": "set NODE_ENV=development&&nodemon --watch backend --watch server ./rtl.js",
"serverUbuntu": "NODE_ENV=development nodemon --watch backend --watch server ./rtl.js", "serverUbuntu": "NODE_ENV=development nodemon --watch backend --watch server ./rtl.js",
"testdev": "ng test --watch=true --code-coverage", "testdev": "ng test --watch=true --code-coverage",
"test": "ng test --watch=false", "test": "ng test --watch=false --browsers=ChromeHeadless",
"lint": "ng lint", "lint": "ng lint",
"lintServer": "eslint ./server/**/* --ext .ts" "lintServer": "eslint ./server/**/* --ext .ts"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^15.0.0",
"@angular/common": "^15.0.0",
"@angular/compiler": "^15.0.0",
"@angular/core": "^15.0.0",
"@angular/forms": "^15.0.0",
"@angular/platform-browser": "^15.0.0",
"@angular/platform-browser-dynamic": "^15.0.0",
"@angular/router": "^15.0.0",
"@ngrx/effects": "^15.0.0", "@ngrx/effects": "^15.0.0",
"@ngrx/store": "^15.0.0", "@ngrx/store": "^15.0.0",
"@swimlane/ngx-charts": "^20.1.2", "@swimlane/ngx-charts": "^20.1.2",
@ -41,7 +33,7 @@
"express-session": "^1.17.3", "express-session": "^1.17.3",
"hocon-parser": "^1.0.1", "hocon-parser": "^1.0.1",
"ini": "^3.0.1", "ini": "^3.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.0",
"ng-qrcode": "^8.0.1", "ng-qrcode": "^8.0.1",
"ngx-perfect-scrollbar-next": "^10.1.1", "ngx-perfect-scrollbar-next": "^10.1.1",
"otplib": "^12.0.1", "otplib": "^12.0.1",
@ -62,11 +54,19 @@
"@angular-eslint/eslint-plugin-template": "^15.1.0", "@angular-eslint/eslint-plugin-template": "^15.1.0",
"@angular-eslint/schematics": "^15.1.0", "@angular-eslint/schematics": "^15.1.0",
"@angular-eslint/template-parser": "^15.1.0", "@angular-eslint/template-parser": "^15.1.0",
"@angular/animations": "^15.0.0",
"@angular/cdk": "^15.0.1", "@angular/cdk": "^15.0.1",
"@angular/cli": "~15.0.1", "@angular/cli": "~15.0.1",
"@angular/common": "^15.0.0",
"@angular/compiler": "^15.0.0",
"@angular/compiler-cli": "^15.0.0", "@angular/compiler-cli": "^15.0.0",
"@angular/core": "^15.0.0",
"@angular/flex-layout": "^14.0.0-beta.41", "@angular/flex-layout": "^14.0.0-beta.41",
"@angular/forms": "^15.0.0",
"@angular/material": "^15.0.1", "@angular/material": "^15.0.1",
"@angular/platform-browser": "^15.0.0",
"@angular/platform-browser-dynamic": "^15.0.0",
"@angular/router": "^15.0.0",
"@fortawesome/angular-fontawesome": "^0.12.0", "@fortawesome/angular-fontawesome": "^0.12.0",
"@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1", "@fortawesome/free-regular-svg-icons": "^6.2.1",

@ -1,8 +1,9 @@
import http from 'http'; import http from 'http';
import App from './backend/utils/app.js';
import { Logger } from './backend/utils/logger.js'; import { Logger } from './backend/utils/logger.js';
import { Common } from './backend/utils/common.js'; import { Common } from './backend/utils/common.js';
import { Config } from './backend/utils/config.js'; // Follow sequence to set server configuration in time
import { WSServer } from './backend/utils/webSocketServer.js'; import { WSServer } from './backend/utils/webSocketServer.js';
import App from './backend/utils/app.js';
const logger = Logger; const logger = Logger;
const common = Common; const common = Common;
@ -13,11 +14,11 @@ const onError = (error) => {
if (error.syscall !== 'listen') { throw error; } if (error.syscall !== 'listen') { throw error; }
switch (error.code) { switch (error.code) {
case 'EACCES': case 'EACCES':
logger.log({ level: 'ERROR', fileName: 'RTL', msg: 'http://' + (common.host ? common.host : 'localhost') + ':' + common.port + ' requires elevated privileges' }); logger.log({ level: 'ERROR', fileName: 'RTL', msg: 'http://' + ((common.host && common.host !== '') ? common.host : 'localhost') + ':' + common.port + ' requires elevated privileges' });
process.exit(1); process.exit(1);
break; break;
case 'EADDRINUSE': case 'EADDRINUSE':
logger.log({ level: 'ERROR', fileName: 'RTL', msg: 'http://' + (common.host ? common.host : 'localhost') + ':' + common.port + ' is already in use' }); logger.log({ level: 'ERROR', fileName: 'RTL', msg: 'http://' + ((common.host && common.host !== '') ? common.host : 'localhost') + ':' + common.port + ' is already in use' });
process.exit(1); process.exit(1);
break; break;
case 'ECONNREFUSED': case 'ECONNREFUSED':
@ -35,7 +36,7 @@ const onError = (error) => {
}; };
const onListening = () => { const onListening = () => {
logger.log({ level: 'INFO', fileName: 'RTL', msg: 'Server is up and running, please open the UI at http://' + (common.host ? common.host : 'localhost') + ':' + common.port + ' or your proxy configured url' }); logger.log({ level: 'INFO', fileName: 'RTL', msg: 'Server is up and running, please open the UI at http://' + ((common.host && common.host !== '') ? common.host : 'localhost') + ':' + common.port + ' or your proxy configured url' });
}; };
let server = http.createServer(app.getApp()); let server = http.createServer(app.getApp());
@ -45,7 +46,7 @@ server.on('listening', onListening);
wsServer.mount(server); wsServer.mount(server);
if (common.host) { if (common.host && common.host !== '') {
server.listen(common.port, common.host); server.listen(common.port, common.host);
} else { } else {
server.listen(common.port); server.listen(common.port);

@ -5,17 +5,41 @@ let options = null;
const logger: LoggerService = Logger; const logger: LoggerService = Logger;
const common: CommonService = Common; const common: CommonService = 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.ln_server_url + '/v1/channel/listPeerChannels';
request(options).then((body) => {
body?.map((channel) => {
if (!channel.alias || channel.alias === '') { channel.alias = channel.peer_id.substring(0, 20); }
const local = channel.to_us_msat || 0;
const remote = (channel.total_msat - local) || 0;
const total = channel.total_msat || 0;
channel.to_them_msat = remote;
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
return channel;
});
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Peer Channels List Received', data: body });
res.status(200).json(body);
}).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 listChannels = (req, res, next) => { export const listChannels = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channels..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channels..' });
options = common.getOptions(req); options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); } if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.ln_server_url + '/v1/channel/listChannels'; options.url = req.session.selectedNode.ln_server_url + '/v1/channel/listPeerChannels';
request(options).then((body) => { request(options).then((body) => {
body?.map((channel) => { body?.map((channel) => {
if (!channel.alias || channel.alias === '') { channel.alias = channel.id.substring(0, 20); } if (!channel.alias || channel.alias === '') { channel.alias = channel.id.substring(0, 20); }
const local = (channel.msatoshi_to_us) ? channel.msatoshi_to_us : 0; const local = channel.to_us_msat || 0;
const remote = (channel.msatoshi_to_them) ? channel.msatoshi_to_them : 0; const remote = (channel.total_msat - local) || 0;
const total = channel.msatoshi_total ? channel.msatoshi_total : 0; const total = channel.total_msat || 0;
channel.to_them_msat = remote;
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3); channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
return channel; return channel;
}); });

@ -22,8 +22,8 @@ function paymentReducer(accumulator, currentPayment) {
function summaryReducer(accumulator, mpp) { function summaryReducer(accumulator, mpp) {
if (mpp.status === 'complete') { if (mpp.status === 'complete') {
accumulator.msatoshi = accumulator.msatoshi + mpp.msatoshi; accumulator.amount_msat = accumulator.amount_msat + mpp.amount_msat;
accumulator.msatoshi_sent = accumulator.msatoshi_sent + mpp.msatoshi_sent; accumulator.amount_sent_msat = accumulator.amount_sent_msat + mpp.amount_sent_msat;
accumulator.status = mpp.status; accumulator.status = mpp.status;
} }
if (mpp.bolt11) { accumulator.bolt11 = mpp.bolt11; } if (mpp.bolt11) { accumulator.bolt11 = mpp.bolt11; }
@ -43,10 +43,10 @@ function groupBy(payments) {
temp.total_parts = 1; temp.total_parts = 1;
delete temp.partid; delete temp.partid;
} else { } else {
const paySummary = curr?.reduce(summaryReducer, { msatoshi: 0, msatoshi_sent: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' }); const paySummary = curr?.reduce(summaryReducer, { amount_msat: 0, amount_sent_msat: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' });
temp = { temp = {
is_group: true, is_expanded: false, total_parts: (curr.length ? curr.length : 0), status: paySummary.status, payment_hash: curr[0].payment_hash, 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, msatoshi: paySummary.msatoshi, msatoshi_sent: paySummary.msatoshi_sent, created_at: curr[0].created_at, destination: curr[0].destination, amount_msat: paySummary.amount_msat, amount_sent_msat: paySummary.amount_sent_msat, created_at: curr[0].created_at,
mpps: curr mpps: curr
}; };
if (paySummary.bolt11) { temp.bolt11 = paySummary.bolt11; } if (paySummary.bolt11) { temp.bolt11 = paySummary.bolt11; }
@ -92,7 +92,7 @@ export const postPayment = (req, res, next) => {
if (req.body.paymentType === 'OFFER') { if (req.body.paymentType === 'OFFER') {
if (req.body.saveToDB && req.body.bolt12) { if (req.body.saveToDB && req.body.bolt12) {
const offerToUpdate: Offer = { bolt12: req.body.bolt12, amountMSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() }; const offerToUpdate: Offer = { bolt12: req.body.bolt12, amountMSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() };
if (req.body.vendor) { offerToUpdate['vendor'] = req.body.vendor; } if (req.body.issuer) { offerToUpdate['issuer'] = req.body.issuer; }
if (req.body.description) { offerToUpdate['description'] = req.body.description; } if (req.body.description) { offerToUpdate['description'] = req.body.description; }
// eslint-disable-next-line arrow-body-style // eslint-disable-next-line arrow-body-style
return databaseService.validateDocument(CollectionsEnum.OFFERS, offerToUpdate).then((validated) => { return databaseService.validateDocument(CollectionsEnum.OFFERS, offerToUpdate).then((validated) => {

@ -52,7 +52,7 @@ export class CLWebSocketClient {
this.connectWithClient(clientExists); this.connectWithClient(clientExists);
} }
} }
} catch (err) { } catch (err: any) {
throw new Error(err); throw new Error(err);
} }
}; };
@ -85,15 +85,11 @@ export class CLWebSocketClient {
}; };
clWsClt.webSocketClient.onerror = (err) => { clWsClt.webSocketClient.onerror = (err) => {
if (clWsClt.selectedNode.api_version === '' || !clWsClt.selectedNode.api_version || this.common.isVersionCompatible(clWsClt.selectedNode.api_version, '0.6.0')) { this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'ERROR', fileName: 'CLWebSocket', msg: 'Web socket error', error: err });
this.logger.log({ selectedNode: clWsClt.selectedNode, level: 'ERROR', fileName: 'CLWebSocket', msg: 'Web socket error', error: err }); const errStr = ((typeof err === 'object' && err.message) ? JSON.stringify({ error: err.message }) : (typeof err === 'object') ? JSON.stringify({ error: err }) : ('{ "error": ' + err + ' }'));
const errStr = ((typeof err === 'object' && err.message) ? JSON.stringify({ error: err.message }) : (typeof err === 'object') ? JSON.stringify({ error: err }) : ('{ "error": ' + err + ' }')); this.wsServer.sendErrorToAllLNClients(errStr, clWsClt.selectedNode);
this.wsServer.sendErrorToAllLNClients(errStr, clWsClt.selectedNode); clWsClt.webSocketClient.close();
clWsClt.webSocketClient.close(); if (clWsClt.reConnect) { this.reconnet(clWsClt); }
if (clWsClt.reConnect) { this.reconnet(clWsClt); }
} else {
clWsClt.reConnect = false;
}
}; };
}; };

@ -2,6 +2,10 @@ import request from 'request-promise';
import { Logger, LoggerService } from '../../utils/logger.js'; import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js'; import { Common, CommonService } from '../../utils/common.js';
import { CommonSelectedNode } from '../../models/config.model.js'; import { CommonSelectedNode } from '../../models/config.model.js';
import { createInvoiceRequestCall, listPendingInvoicesRequestCall } from './invoices.js';
import { findRouteBetweenNodesRequestCall } from './network.js';
import { getSentInfoFromPaymentRequest, sendPaymentToRouteRequestCall } from './payments.js';
let options = null; let options = null;
const logger: LoggerService = Logger; const logger: LoggerService = Logger;
const common: CommonService = Common; const common: CommonService = Common;
@ -151,3 +155,54 @@ export const closeChannel = (req, res, next) => {
}); });
}; };
// options.form = { sourceNodeId: req.params.source, targetNodeId: req.params.target, amountMsat: req.params.amount, ignoreNodeIds: req.params.ignore };
export const circularRebalance = (req, res, next) => {
const crInvDescription = 'Circular rebalancing invoice for ' + (req.body.amountMsat / 1000) + ' Sats';
options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.form = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Rebalance Params', data: options.form });
const tillToday = (Math.round(new Date(Date.now()).getTime() / 1000)).toString();
// Check if unpaid Invoice exists already
listPendingInvoicesRequestCall(req.session.selectedNode).then((callRes: any[]) => {
const foundExistingInvoice = callRes.find((inv) => inv.description.includes(crInvDescription) && inv.amount === req.body.amountMsat && inv.expiry && inv.timestamp && ((inv.expiry + inv.timestamp) >= tillToday));
// Create new invoice if doesn't exist already
const requestCalls = foundExistingInvoice && foundExistingInvoice.serialized ?
[findRouteBetweenNodesRequestCall(req.session.selectedNode, req.body.amountMsat, req.body.sourceNodeId, req.body.targetNodeId, req.body.ignoreNodeIds, req.body.format)] :
[findRouteBetweenNodesRequestCall(req.session.selectedNode, req.body.amountMsat, req.body.sourceNodeId, req.body.targetNodeId, req.body.ignoreNodeIds, req.body.format), createInvoiceRequestCall(req.session.selectedNode, crInvDescription, req.body.amountMsat)];
Promise.all(requestCalls).then((values: any[]) => {
// eslint-disable-next-line arrow-body-style
const routes = values[0]?.routes?.filter((route) => {
return !((route.shortChannelIds[0] === req.body.sourceShortChannelId && route.shortChannelIds[1] === req.body.targetShortChannelId) ||
(route.shortChannelIds[1] === req.body.sourceShortChannelId && route.shortChannelIds[0] === req.body.targetShortChannelId));
});
const firstRoute = routes[0].shortChannelIds.join() || '';
const shortChannelIds = req.body.sourceShortChannelId + ',' + firstRoute + ',' + req.body.targetShortChannelId;
const invoice = (foundExistingInvoice && foundExistingInvoice.serialized ? foundExistingInvoice.serialized : (values[1] ? values[1].serialized : '')) || '';
const paymentHash = (foundExistingInvoice && foundExistingInvoice.paymentHash ? foundExistingInvoice.paymentHash : (values[1] ? values[1].paymentHash : '') || '');
return sendPaymentToRouteRequestCall(req.session.selectedNode, shortChannelIds, invoice, req.body.amountMsat).then((payToRouteCallRes) => {
// eslint-disable-next-line arrow-body-style
setTimeout(() => {
return getSentInfoFromPaymentRequest(req.session.selectedNode, paymentHash).then((sentInfoCallRes) => {
const payStatus = sentInfoCallRes.length && sentInfoCallRes.length > 0 ? sentInfoCallRes[sentInfoCallRes.length - 1].status : sentInfoCallRes;
return res.status(201).json({ flgReusingInvoice: !!foundExistingInvoice, invoice: invoice, paymentHash: paymentHash, paymentDetails: payToRouteCallRes, paymentStatus: payStatus });
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Channel Rebalance From Sent Info Error', req.session.selectedNode);
return res.status(err.statusCode).json({ flgReusingInvoice: !!foundExistingInvoice, invoice: invoice, paymentHash: paymentHash, paymentDetails: payToRouteCallRes, paymentStatus: err.error });
});
}, 3000);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Channel Rebalance From Send Payment To Route Error', req.session.selectedNode);
return res.status(err.statusCode).json({ flgReusingInvoice: !!foundExistingInvoice, invoice: invoice, paymentHash: paymentHash, paymentDetails: {}, paymentStatus: err.error });
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Channel Rebalance From Find Routes Error', req.session.selectedNode);
return res.status(err.statusCode).json({ flgReusingInvoice: !!foundExistingInvoice, invoice: (foundExistingInvoice.serialized || ''), paymentHash: '', paymentDetails: {}, paymentStatus: err.error });
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Channel Rebalance From List Pending Invoices Error', req.session.selectedNode);
return res.status(err.statusCode).json({ flgReusingInvoice: false, invoice: '', paymentHash: '', paymentDetails: {}, paymentStatus: err.error });
});
};

@ -1,6 +1,7 @@
import request from 'request-promise'; import request from 'request-promise';
import { Logger, LoggerService } from '../../utils/logger.js'; import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js'; import { Common, CommonService } from '../../utils/common.js';
import { CommonSelectedNode } from 'server/models/config.model.js';
let options = null; let options = null;
const logger: LoggerService = Logger; const logger: LoggerService = Logger;
const common: CommonService = Common; const common: CommonService = Common;
@ -18,7 +19,7 @@ export const getReceivedPaymentInfo = (lnServerUrl, invoice) => {
invoice.status = response.status.type; invoice.status = response.status.type;
if (response.status && response.status.type === 'received') { if (response.status && response.status.type === 'received') {
invoice.amountSettled = response.status.amount ? Math.round(response.status.amount / 1000) : 0; invoice.amountSettled = response.status.amount ? Math.round(response.status.amount / 1000) : 0;
invoice.receivedAt = response.status.receivedAt ? Math.round(response.status.receivedAt / 1000) : 0; invoice.receivedAt = response.status.receivedAt.unix ? response.status.receivedAt.unix : 0;
} }
return invoice; return invoice;
}).catch((err) => { }).catch((err) => {
@ -51,6 +52,21 @@ export const getInvoice = (req, res, next) => {
}); });
}; };
export const listPendingInvoicesRequestCall = (selectedNode: CommonSelectedNode) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'List Pending Invoices..' });
options = selectedNode.options;
options.url = selectedNode.ln_server_url + '/listpendinginvoices';
options.form = { from: 0, to: (Math.round(new Date(Date.now()).getTime() / 1000)).toString() };
return new Promise((resolve, reject) => {
request.post(options).then((pendingInvoicesResponse) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Pending Invoices List ', data: pendingInvoicesResponse });
resolve(pendingInvoicesResponse);
}).catch((errRes) => {
reject(common.handleError(errRes, 'Invoices', 'List Pending Invoices Error', selectedNode));
});
});
};
export const listInvoices = (req, res, next) => { export const listInvoices = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting List Invoices..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting List Invoices..' });
options = common.getOptions(req); options = common.getOptions(req);
@ -98,18 +114,27 @@ export const listInvoices = (req, res, next) => {
} }
}; };
export const createInvoiceRequestCall = (selectedNode: CommonSelectedNode, description: string, amount: number) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = selectedNode.options;
options.url = selectedNode.ln_server_url + '/createinvoice';
options.form = { description: description, amountMsat: amount };
return new Promise((resolve, reject) => {
request.post(options).then((invResponse) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Created', data: invResponse });
if (invResponse.amount) { invResponse.amount = Math.round(invResponse.amount / 1000); }
resolve(invResponse);
}).catch((errRes) => {
reject(common.handleError(errRes, 'Invoices', 'Create Invoice Error', selectedNode));
});
});
};
export const createInvoice = (req, res, next) => { export const createInvoice = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = common.getOptions(req); options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); } if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.ln_server_url + '/createinvoice'; createInvoiceRequestCall(req.session.selectedNode, req.body.description, req.body.amountMsat).then((invRes) => {
options.form = req.body; res.status(201).json(invRes);
request.post(options).then((body) => { }).catch((err) => res.status(err.statusCode).json({ message: err.message, error: err.error }));
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoice Created', data: body });
if (body.amount) { body.amount = Math.round(body.amount / 1000); }
res.status(201).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoices', 'Create Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
}; };

@ -1,6 +1,7 @@
import request from 'request-promise'; import request from 'request-promise';
import { Logger, LoggerService } from '../../utils/logger.js'; import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js'; import { Common, CommonService } from '../../utils/common.js';
import { CommonSelectedNode } from 'server/models/config.model.js';
let options = null; let options = null;
const logger: LoggerService = Logger; const logger: LoggerService = Logger;
const common: CommonService = Common; const common: CommonService = Common;
@ -19,3 +20,26 @@ export const getNodes = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error }); return res.status(err.statusCode).json({ message: err.message, error: err.error });
}); });
}; };
export const findRouteBetweenNodesRequestCall = (selectedNode: CommonSelectedNode, amountMsat: number, sourceNodeId: string, targetNodeId: string, ignoreNodeIds: string[] = [], format: string = 'shortChannelId') => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Network', msg: 'Find Route Between Nodes..' });
options = selectedNode.options;
options.url = selectedNode.ln_server_url + '/findroutebetweennodes';
options.form = { amountMsat: amountMsat, sourceNodeId: sourceNodeId, targetNodeId: targetNodeId, ignoreNodeIds: ignoreNodeIds, format: format };
return new Promise((resolve, reject) => {
request.post(options).then((body) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Network', msg: 'Route Lookup Between Nodes Finished', data: body });
resolve(body);
}).catch((errRes) => {
reject(common.handleError(errRes, 'Network', 'Route Lookup Between Nodes Error', selectedNode));
});
});
};
export const findRouteBetweenNodes = (req, res, next) => {
options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
findRouteBetweenNodesRequestCall(req.session.selectedNode, req.body.amountMsat, req.body.sourceNodeId, req.body.targetNodeId, req.body.ignoreNodeIds, req.body.format).then((callRes) => {
res.status(200).json(callRes);
}).catch((err) => res.status(err.statusCode).json({ message: err.message, error: err.error }));
};

@ -117,3 +117,28 @@ export const getSentPaymentsInformation = (req, res, next) => {
return res.status(200).json([]); return res.status(200).json([]);
} }
}; };
export const sendPaymentToRouteRequestCall = (selectedNode: CommonSelectedNode, shortChannelIds: string, invoice: string, amountMsat: number) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = selectedNode.options;
options.url = selectedNode.ln_server_url + '/sendtoroute';
options.form = { shortChannelIds: shortChannelIds, amountMsat: amountMsat, invoice: invoice };
return new Promise((resolve, reject) => {
logger.log({ selectedNode: selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Send Payment To Route Options', data: options.form });
request.post(options).then((body) => {
logger.log({ selectedNode: selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent To Route', data: body });
resolve(body);
}).catch((errRes) => {
reject(common.handleError(errRes, 'Payments', 'Send Payment To Route Error', selectedNode));
});
});
};
export const sendPaymentToRoute = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Send Payment To Route..' });
options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
sendPaymentToRouteRequestCall(req.session.selectedNode, req.body.shortChannelIds, req.body.invoice, req.body.amountMsat).then((callRes) => {
res.status(200).json(callRes);
}).catch((err) => res.status(err.statusCode).json({ message: err.message, error: err.error }));
};

@ -51,7 +51,7 @@ export class ECLWebSocketClient {
this.connectWithClient(clientExists); this.connectWithClient(clientExists);
} }
} }
} catch (err) { } catch (err: any) {
throw new Error(err); throw new Error(err);
} }
}; };
@ -97,7 +97,6 @@ export class ECLWebSocketClient {
eclWsClt.reConnect = false; eclWsClt.reConnect = false;
} }
}; };
}; };
public disconnect = (selectedNode: CommonSelectedNode) => { public disconnect = (selectedNode: CommonSelectedNode) => {
@ -121,13 +120,14 @@ export class ECLWebSocketClient {
public heartbeat = (eclWsClt) => { public heartbeat = (eclWsClt) => {
this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Websocket Server Heartbeat..' }); this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Websocket Server Heartbeat..' });
if (!eclWsClt.webSocketClient) return; if (!eclWsClt.webSocketClient) { return; }
if (eclWsClt.webSocketClient.readyState !== 1) return; if (eclWsClt.webSocketClient.readyState !== 1) { return; }
eclWsClt.webSocketClient.ping(); eclWsClt.webSocketClient.ping();
setTimeout(() => { setTimeout(() => {
this.heartbeat(eclWsClt); this.heartbeat(eclWsClt);
}, 59 * 1000); }, 59 * 1000);
} };
} }
export const ECLWSClient = new ECLWebSocketClient(); export const ECLWSClient = new ECLWebSocketClient();

@ -166,6 +166,7 @@ export const postTransactions = (req, res, next) => {
options.form = JSON.stringify(options.form); options.form = JSON.stringify(options.form);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form }); logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form });
request.post(options).then((body) => { request.post(options).then((body) => {
body = body.result ? body.result : body;
if (body.payment_error) { if (body.payment_error) {
const err = common.handleError(body.payment_error, 'Channels', 'Send Payment Error', req.session.selectedNode); const err = common.handleError(body.payment_error, 'Channels', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error }); return res.status(err.statusCode).json({ message: err.message, error: err.error });
@ -196,7 +197,7 @@ export const closeChannel = (req, res, next) => {
request.delete(options); request.delete(options);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Close Requested' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Close Requested' });
res.status(202).json({ message: 'Close channel request has been submitted.' }); res.status(202).json({ message: 'Close channel request has been submitted.' });
} catch (error) { } catch (error: any) {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Close Channel Error', error: error.message }); logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Close Channel Error', error: error.message });
return res.status(500).json({ message: 'Close Channel Error', error: error.message }); return res.status(500).json({ message: 'Close Channel Error', error: error.message });
} }

@ -30,7 +30,7 @@ export class LNDWebSocketClient {
const newWebSocketClient = { selectedNode: selectedNode }; const newWebSocketClient = { selectedNode: selectedNode };
this.webSocketClients.push(newWebSocketClient); this.webSocketClients.push(newWebSocketClient);
} }
} catch (err) { } catch (err: any) {
throw new Error(err); throw new Error(err);
} }
}; };

@ -1,7 +1,7 @@
import { Database, DatabaseService } from '../../utils/database.js'; import { Database, DatabaseService } from '../../utils/database.js';
import { Logger, LoggerService } from '../../utils/logger.js'; import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js'; import { Common, CommonService } from '../../utils/common.js';
import { CollectionsEnum, PageSettings } from '../../models/database.model.js'; import { CollectionsEnum } from '../../models/database.model.js';
const logger: LoggerService = Logger; const logger: LoggerService = Logger;
const common: CommonService = Common; const common: CommonService = Common;
@ -9,7 +9,7 @@ const databaseService: DatabaseService = Database;
export const getPageSettings = (req, res, next) => { export const getPageSettings = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Getting Page Settings..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Getting Page Settings..' });
databaseService.find(req.session.selectedNode, CollectionsEnum.PAGE_SETTINGS).then((settings: PageSettings) => { databaseService.find(req.session.selectedNode, CollectionsEnum.PAGE_SETTINGS).then((settings: any) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Page Settings Received', data: settings }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Page Settings Received', data: settings });
res.status(200).json(settings); res.status(200).json(settings);
}).catch((errRes) => { }).catch((errRes) => {

@ -2,7 +2,7 @@ export enum OfferFieldsEnum {
BOLT12 = 'bolt12', BOLT12 = 'bolt12',
AMOUNTMSAT = 'amountMSat', AMOUNTMSAT = 'amountMSat',
TITLE = 'title', TITLE = 'title',
VENDOR = 'vendor', ISSUER = 'issuer',
DESCRIPTION = 'description' DESCRIPTION = 'description'
} }
@ -12,7 +12,7 @@ export class Offer {
public bolt12: string, public bolt12: string,
public amountMSat: number, public amountMSat: number,
public title: string, public title: string,
public vendor?: string, public issuer?: string,
public description?: string, public description?: string,
public lastUpdatedAt?: number public lastUpdatedAt?: number
) { } ) { }

@ -1,12 +1,11 @@
export enum ECLWSEventsEnum { export enum ECLWSEventsEnum {
PAY_RECEIVED = 'payment-received', PAY_RECEIVED = 'payment-received',
PAY_RELAYED = 'payment-relayed', PAY_RELAYED = 'payment-relayed',
PAY_SENT = 'payment-sent', PAY_SENT = 'payment-sent',
PAY_SETTLING_ONCHAIN = 'payment-settling-onchain', PAY_SETTLING_ONCHAIN = 'payment-settling-onchain',
PAY_FAILED = 'payment-failed', PAY_FAILED = 'payment-failed',
CHANNEL_OPENED = 'channel-opened', CHANNEL_OPENED = 'channel-opened',
CHANNEL_STATE_CHANGED = 'channel-state-changed', CHANNEL_STATE_CHANGED = 'channel-state-changed',
CHANNEL_CLOSED = 'channel-closed', CHANNEL_CLOSED = 'channel-closed',
ONION_MESSAGE_RECEIVED = 'onion-message-received' ONION_MESSAGE_RECEIVED = 'onion-message-received'
} }

@ -1,11 +1,12 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; import { isAuthenticated } from '../../utils/authCheck.js';
import { listChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards, funderUpdatePolicy, listForwardsPaginated } from '../../controllers/cln/channels.js'; import { listChannels, listPeerChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards, funderUpdatePolicy, listForwardsPaginated } from '../../controllers/cln/channels.js';
const router = Router(); const router = Router();
router.get('/listChannels', isAuthenticated, listChannels); router.get('/listChannels', isAuthenticated, listChannels);
router.get('/listPeerChannels', isAuthenticated, listPeerChannels);
router.post('/', isAuthenticated, openChannel); router.post('/', isAuthenticated, openChannel);
router.post('/setChannelFee', isAuthenticated, setChannelFee); router.post('/setChannelFee', isAuthenticated, setChannelFee);
router.delete('/:channelId', isAuthenticated, closeChannel); router.delete('/:channelId', isAuthenticated, closeChannel);

@ -1,7 +1,7 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; import { isAuthenticated } from '../../utils/authCheck.js';
import { getChannels, getChannelStats, openChannel, updateChannelRelayFee, closeChannel } from '../../controllers/eclair/channels.js'; import { getChannels, getChannelStats, openChannel, updateChannelRelayFee, closeChannel, circularRebalance } from '../../controllers/eclair/channels.js';
const router = Router(); const router = Router();
@ -9,6 +9,7 @@ router.get('/', isAuthenticated, getChannels);
router.get('/stats', isAuthenticated, getChannelStats); router.get('/stats', isAuthenticated, getChannelStats);
router.post('/', isAuthenticated, openChannel); router.post('/', isAuthenticated, openChannel);
router.post('/updateRelayFee', isAuthenticated, updateChannelRelayFee); router.post('/updateRelayFee', isAuthenticated, updateChannelRelayFee);
router.post('/circularRebalance', circularRebalance);
router.delete('/', isAuthenticated, closeChannel); router.delete('/', isAuthenticated, closeChannel);
export default router; export default router;

@ -1,10 +1,10 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; import { isAuthenticated } from '../../utils/authCheck.js';
import { getNodes } from '../../controllers/eclair/network.js'; import { getNodes, findRouteBetweenNodes } from '../../controllers/eclair/network.js';
const router = Router(); const router = Router();
router.get('/nodes/:id', isAuthenticated, getNodes); router.get('/nodes/:id', isAuthenticated, getNodes);
router.get('/routebetweennodes', isAuthenticated, findRouteBetweenNodes);
export default router; export default router;

@ -1,13 +1,14 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; import { isAuthenticated } from '../../utils/authCheck.js';
import { queryPaymentRoute, decodePayment, getSentPaymentsInformation, postPayment } from '../../controllers/eclair/payments.js'; import { queryPaymentRoute, decodePayment, getSentPaymentsInformation, postPayment, sendPaymentToRoute } from '../../controllers/eclair/payments.js';
const router = Router(); const router = Router();
router.get('/route/', isAuthenticated, queryPaymentRoute); router.get('/route/', isAuthenticated, queryPaymentRoute);
router.get('/decode/:invoice', isAuthenticated, decodePayment); router.get('/decode/:invoice', isAuthenticated, decodePayment);
router.post('/getsentinfos', isAuthenticated, getSentPaymentsInformation); router.post('/getsentinfos', isAuthenticated, getSentPaymentsInformation);
router.post('/sendtoroute', isAuthenticated, sendPaymentToRoute);
router.post('/', isAuthenticated, postPayment); router.post('/', isAuthenticated, postPayment);
export default router; export default router;

@ -13,7 +13,6 @@ import clnRoutes from '../routes/cln/index.js';
import eclRoutes from '../routes/eclair/index.js'; import eclRoutes from '../routes/eclair/index.js';
import { Common, CommonService } from './common.js'; import { Common, CommonService } from './common.js';
import { Logger, LoggerService } from './logger.js'; import { Logger, LoggerService } from './logger.js';
import { Config, ConfigService } from './config.js';
import { CLWSClient, CLWebSocketClient } from '../controllers/cln/webSocketClient.js'; import { CLWSClient, CLWebSocketClient } from '../controllers/cln/webSocketClient.js';
import { ECLWSClient, ECLWebSocketClient } from '../controllers/eclair/webSocketClient.js'; import { ECLWSClient, ECLWebSocketClient } from '../controllers/eclair/webSocketClient.js';
import { LNDWSClient, LNDWebSocketClient } from '../controllers/lnd/webSocketClient.js'; import { LNDWSClient, LNDWebSocketClient } from '../controllers/lnd/webSocketClient.js';
@ -25,7 +24,6 @@ export class ExpressApplication {
public app = express(); public app = express();
public logger: LoggerService = Logger; public logger: LoggerService = Logger;
public common: CommonService = Common; public common: CommonService = Common;
public config: ConfigService = Config;
public eclWsClient: ECLWebSocketClient = ECLWSClient; public eclWsClient: ECLWebSocketClient = ECLWSClient;
public clWsClient: CLWebSocketClient = CLWSClient; public clWsClient: CLWebSocketClient = CLWSClient;
public lndWsClient: LNDWebSocketClient = LNDWSClient; public lndWsClient: LNDWebSocketClient = LNDWSClient;
@ -39,7 +37,6 @@ export class ExpressApplication {
this.app.use(bodyParser.json({ limit: '25mb' })); this.app.use(bodyParser.json({ limit: '25mb' }));
this.app.use(bodyParser.urlencoded({ extended: false, limit: '25mb' })); this.app.use(bodyParser.urlencoded({ extended: false, limit: '25mb' }));
this.loadConfiguration();
this.setCORS(); this.setCORS();
this.setCSRF(); this.setCSRF();
this.setApplicationRoutes(); this.setApplicationRoutes();
@ -47,10 +44,6 @@ export class ExpressApplication {
public getApp = () => this.app; public getApp = () => this.app;
public loadConfiguration = () => {
this.config.setServerConfiguration();
};
public setCORS = () => { CORS.mount(this.app); }; public setCORS = () => { CORS.mount(this.app); };
public setCSRF = () => { CSRF.mount(this.app); }; public setCSRF = () => { CSRF.mount(this.app); };

@ -13,7 +13,8 @@ export class CommonService {
public initSelectedNode: CommonSelectedNode = null; public initSelectedNode: CommonSelectedNode = null;
public rtl_conf_file_path = ''; public rtl_conf_file_path = '';
public port = 3000; public port = 3000;
public host = null; public host = '';
public db_directory_path = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
public rtl_pass = ''; public rtl_pass = '';
public flg_allow_password_update = true; public flg_allow_password_update = true;
public rtl_secret2fa = ''; public rtl_secret2fa = '';
@ -236,7 +237,11 @@ export class CommonService {
}; };
public handleError = (errRes, fileName, errMsg, selectedNode: CommonSelectedNode) => { public handleError = (errRes, fileName, errMsg, selectedNode: CommonSelectedNode) => {
const err = JSON.parse(JSON.stringify(errRes)); let err = JSON.parse(JSON.stringify(errRes));
if (err && err.error && Object.keys(err.error).length === 0 && errRes.error && (errRes.error.stack || errRes.error.message)) {
errRes.error = errRes.error.stack || errRes.error.message;
err = JSON.parse(JSON.stringify(errRes));
}
if (!selectedNode) { selectedNode = this.initSelectedNode; } if (!selectedNode) { selectedNode = this.initSelectedNode; }
switch (selectedNode.ln_implementation) { switch (selectedNode.ln_implementation) {
case 'LND': case 'LND':
@ -270,7 +275,7 @@ export class CommonService {
if (err.options && err.options.headers) { delete err.options.headers; } if (err.options && err.options.headers) { delete err.options.headers; }
break; break;
} }
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') }); this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : err) });
let newErrorObj = { statusCode: 500, message: '', error: '' }; let newErrorObj = { statusCode: 500, message: '', error: '' };
if (err.code && err.code === 'ENOENT') { if (err.code && err.code === 'ENOENT') {
newErrorObj = { newErrorObj = {
@ -330,7 +335,7 @@ export class CommonService {
try { try {
this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8'); this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8');
} catch (err) { } catch (err) {
this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while reading cookie: \n' + err }); this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Something went wrong while reading cookie: \n' + err });
throw new Error(err); throw new Error(err);
} }
} else { } else {
@ -340,7 +345,7 @@ export class CommonService {
fs.writeFileSync(this.rtl_cookie_path, crypto.randomBytes(64).toString('hex')); fs.writeFileSync(this.rtl_cookie_path, crypto.randomBytes(64).toString('hex'));
this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8'); this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8');
} catch (err) { } catch (err) {
this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while reading the cookie: \n' + err }); this.logger.log({ selectedNode: this.initSelectedNode, level: 'ERROR', fileName: 'Common', msg: 'Something went wrong while reading the cookie: \n' + err });
throw new Error(err); throw new Error(err);
} }
} }
@ -441,6 +446,7 @@ export class CommonService {
if (selNode && selNode.index) { if (selNode && selNode.index) {
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'PORT: ' + this.port }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'PORT: ' + this.port });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'HOST: ' + this.host }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'HOST: ' + this.host });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'DB_DIRECTORY_PATH: ' + this.db_directory_path });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'SSO: ' + this.rtl_sso }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'SSO: ' + this.rtl_sso });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'DEFAULT NODE INDEX: ' + selNode.index }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'DEFAULT NODE INDEX: ' + selNode.index });
this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'INDEX: ' + selNode.index }); this.logger.log({ selectedNode: selNode, level: 'INFO', fileName: 'Config Setup Variable', msg: 'INDEX: ' + selNode.index });

@ -17,38 +17,46 @@ export class ConfigService {
private common: CommonService = Common; private common: CommonService = Common;
private logger: LoggerService = Logger; private logger: LoggerService = Logger;
constructor() { } constructor() {
this.setServerConfiguration();
}
private setDefaultConfig = () => { private setDefaultConfig = () => {
const homeDir = os.userInfo().homedir; const homeDir = os.userInfo().homedir;
let macaroonPath = ''; let macaroonPath = '';
let configPath = ''; let configPath = '';
let channelBackupPath = ''; let channelBackupPath = '';
let dbPath = '';
switch (this.platform) { switch (this.platform) {
case 'win32': case 'win32':
macaroonPath = homeDir + '\\AppData\\Local\\Lnd\\data\\chain\\bitcoin\\mainnet'; macaroonPath = homeDir + '\\AppData\\Local\\Lnd\\data\\chain\\bitcoin\\mainnet';
configPath = homeDir + '\\AppData\\Local\\Lnd\\lnd.conf'; configPath = homeDir + '\\AppData\\Local\\Lnd\\lnd.conf';
channelBackupPath = homeDir + '\\backup\\node-1'; channelBackupPath = homeDir + '\\backup\\node-1';
dbPath = homeDir + '\\database\\node-1';
break; break;
case 'darwin': case 'darwin':
macaroonPath = homeDir + '/Library/Application Support/Lnd/data/chain/bitcoin/mainnet'; macaroonPath = homeDir + '/Library/Application Support/Lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/Library/Application Support/Lnd/lnd.conf'; configPath = homeDir + '/Library/Application Support/Lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1'; channelBackupPath = homeDir + '/backup/node-1';
dbPath = homeDir + '/database/node-1';
break; break;
case 'linux': case 'linux':
macaroonPath = homeDir + '/.lnd/data/chain/bitcoin/mainnet'; macaroonPath = homeDir + '/.lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/.lnd/lnd.conf'; configPath = homeDir + '/.lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1'; channelBackupPath = homeDir + '/backup/node-1';
dbPath = homeDir + '/database/node-1';
break; break;
default: default:
macaroonPath = ''; macaroonPath = '';
configPath = ''; configPath = '';
channelBackupPath = ''; channelBackupPath = '';
dbPath = '';
break; break;
} }
const configData = { const configData = {
port: '3000', port: '3000',
defaultNodeIndex: 1, defaultNodeIndex: 1,
dbDirectoryPath: dbPath,
SSO: { SSO: {
rtlSSO: 0, rtlSSO: 0,
rtlCookiePath: '', rtlCookiePath: '',
@ -76,7 +84,7 @@ export class ConfigService {
} }
] ]
}; };
if (+process.env.RTL_SSO === 0 || configData.SSO.rtlSSO === 0) { if ((process?.env?.RTL_SSO && +process?.env?.RTL_SSO === 0) || configData.SSO.rtlSSO === 0) {
configData['multiPass'] = 'password'; configData['multiPass'] = 'password';
} }
return configData; return configData;
@ -95,7 +103,7 @@ export class ConfigService {
private updateLogByLevel = () => { private updateLogByLevel = () => {
let updateLogFlag = false; let updateLogFlag = false;
this.common.rtl_conf_file_path = process.env.RTL_CONFIG_PATH ? process.env.RTL_CONFIG_PATH : join(this.directoryName, '../..'); this.common.rtl_conf_file_path = process?.env?.RTL_CONFIG_PATH ? process?.env?.RTL_CONFIG_PATH : join(this.directoryName, '../..');
try { try {
const RTLConfFile = this.common.rtl_conf_file_path + sep + 'RTL-Config.json'; const RTLConfFile = this.common.rtl_conf_file_path + sep + 'RTL-Config.json';
const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8')); const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8'));
@ -115,9 +123,9 @@ export class ConfigService {
}; };
private validateNodeConfig = (config) => { private validateNodeConfig = (config) => {
if ((+process.env.RTL_SSO === 0) || (typeof process.env.RTL_SSO === 'undefined' && +config.SSO.rtlSSO === 0)) { if ((process?.env?.RTL_SSO && +process?.env?.RTL_SSO === 0) || (typeof process?.env?.RTL_SSO === 'undefined' && +config.SSO.rtlSSO === 0)) {
if (process.env.APP_PASSWORD && process.env.APP_PASSWORD.trim() !== '') { if (process?.env?.APP_PASSWORD && process?.env?.APP_PASSWORD.trim() !== '') {
this.common.rtl_pass = this.hash.update(process.env.APP_PASSWORD).digest('hex'); this.common.rtl_pass = this.hash.update(process?.env?.APP_PASSWORD).digest('hex');
this.common.flg_allow_password_update = false; this.common.flg_allow_password_update = false;
} else if (config.multiPassHashed && config.multiPassHashed !== '') { } else if (config.multiPassHashed && config.multiPassHashed !== '') {
this.common.rtl_pass = config.multiPassHashed; this.common.rtl_pass = config.multiPassHashed;
@ -128,21 +136,22 @@ export class ConfigService {
} }
this.common.rtl_secret2fa = config.secret2fa; this.common.rtl_secret2fa = config.secret2fa;
} else { } else {
if (process.env.APP_PASSWORD && process.env.APP_PASSWORD.trim() !== '') { if (process?.env?.APP_PASSWORD && process?.env?.APP_PASSWORD.trim() !== '') {
this.errMsg = this.errMsg + '\nRTL Password cannot be set with SSO. Please set SSO as 0 or remove password.'; this.errMsg = this.errMsg + '\nRTL Password cannot be set with SSO. Please set SSO as 0 or remove password.';
} }
} }
this.common.port = (process.env.PORT) ? this.normalizePort(process.env.PORT) : (config.port) ? this.normalizePort(config.port) : 3000; this.common.port = (process?.env?.PORT) ? this.normalizePort(process?.env?.PORT) : (config.port) ? this.normalizePort(config.port) : 3000;
this.common.host = (process.env.HOST) ? process.env.HOST : (config.host) ? config.host : null; this.common.host = (process?.env?.HOST) ? process?.env?.HOST : (config.host) ? config.host : null;
this.common.db_directory_path = (process?.env?.DB_DIRECTORY_PATH) ? process?.env?.DB_DIRECTORY_PATH : (config.dbDirectoryPath) ? config.dbDirectoryPath : join(dirname(fileURLToPath(import.meta.url)), '..', '..');
if (config.nodes && config.nodes.length > 0) { if (config.nodes && config.nodes.length > 0) {
config.nodes.forEach((node, idx) => { config.nodes.forEach((node, idx) => {
this.common.nodes[idx] = {}; this.common.nodes[idx] = {};
this.common.nodes[idx].index = node.index; this.common.nodes[idx].index = node.index;
this.common.nodes[idx].ln_node = node.lnNode; this.common.nodes[idx].ln_node = node.lnNode;
this.common.nodes[idx].ln_implementation = (process.env.LN_IMPLEMENTATION) ? process.env.LN_IMPLEMENTATION : node.lnImplementation ? node.lnImplementation : 'LND'; this.common.nodes[idx].ln_implementation = (process?.env?.LN_IMPLEMENTATION) ? process?.env?.LN_IMPLEMENTATION : node.lnImplementation ? node.lnImplementation : 'LND';
if (this.common.nodes[idx].ln_implementation === 'CLT') { this.common.nodes[idx].ln_implementation = 'CLN'; } if (this.common.nodes[idx].ln_implementation === 'CLT') { this.common.nodes[idx].ln_implementation = 'CLN'; }
if (this.common.nodes[idx].ln_implementation !== 'ECL' && process.env.MACAROON_PATH && process.env.MACAROON_PATH.trim() !== '') { if (this.common.nodes[idx].ln_implementation !== 'ECL' && process?.env?.MACAROON_PATH && process?.env?.MACAROON_PATH.trim() !== '') {
this.common.nodes[idx].macaroon_path = process.env.MACAROON_PATH; this.common.nodes[idx].macaroon_path = process?.env?.MACAROON_PATH;
} else if (this.common.nodes[idx].ln_implementation !== 'ECL' && node.Authentication && node.Authentication.macaroonPath && node.Authentication.macaroonPath.trim() !== '') { } else if (this.common.nodes[idx].ln_implementation !== 'ECL' && node.Authentication && node.Authentication.macaroonPath && node.Authentication.macaroonPath.trim() !== '') {
this.common.nodes[idx].macaroon_path = node.Authentication.macaroonPath; this.common.nodes[idx].macaroon_path = node.Authentication.macaroonPath;
} else if (this.common.nodes[idx].ln_implementation !== 'ECL') { } else if (this.common.nodes[idx].ln_implementation !== 'ECL') {
@ -150,16 +159,16 @@ export class ConfigService {
} }
if (this.common.nodes[idx].ln_implementation === 'ECL') { if (this.common.nodes[idx].ln_implementation === 'ECL') {
if (process.env.LN_API_PASSWORD) { if (process?.env?.LN_API_PASSWORD) {
this.common.nodes[idx].ln_api_password = process.env.LN_API_PASSWORD; this.common.nodes[idx].ln_api_password = process?.env?.LN_API_PASSWORD;
} else if (node.Authentication && node.Authentication.lnApiPassword) { } else if (node.Authentication && node.Authentication.lnApiPassword) {
this.common.nodes[idx].ln_api_password = node.Authentication.lnApiPassword; this.common.nodes[idx].ln_api_password = node.Authentication.lnApiPassword;
} else { } else {
this.common.nodes[idx].ln_api_password = ''; this.common.nodes[idx].ln_api_password = '';
} }
} }
if (process.env.CONFIG_PATH) { if (process?.env?.CONFIG_PATH) {
this.common.nodes[idx].config_path = process.env.CONFIG_PATH; this.common.nodes[idx].config_path = process?.env?.CONFIG_PATH;
} else if (node.Authentication && node.Authentication.configPath) { } else if (node.Authentication && node.Authentication.configPath) {
this.common.nodes[idx].config_path = node.Authentication.configPath; this.common.nodes[idx].config_path = node.Authentication.configPath;
} else { } else {
@ -167,10 +176,10 @@ export class ConfigService {
} }
if (this.common.nodes[idx].ln_implementation === 'ECL' && this.common.nodes[idx].ln_api_password === '' && this.common.nodes[idx].config_path !== '') { if (this.common.nodes[idx].ln_implementation === 'ECL' && this.common.nodes[idx].ln_api_password === '' && this.common.nodes[idx].config_path !== '') {
try { try {
const exists = fs.existsSync(this.common.nodes[idx].config_path); const exists = fs.existsSync(this.common.nodes[idx].config_path || '');
if (exists) { if (exists) {
try { try {
const configFile = fs.readFileSync(this.common.nodes[idx].config_path, 'utf-8'); const configFile = fs.readFileSync((this.common.nodes[idx].config_path || ''), 'utf-8');
const iniParsed = ini.parse(configFile); const iniParsed = ini.parse(configFile);
this.common.nodes[idx].ln_api_password = iniParsed['eclair.api.password'] ? iniParsed['eclair.api.password'] : parseHocon(configFile).eclair.api.password; this.common.nodes[idx].ln_api_password = iniParsed['eclair.api.password'] ? iniParsed['eclair.api.password'] : parseHocon(configFile).eclair.api.password;
} catch (err) { } catch (err) {
@ -187,10 +196,10 @@ export class ConfigService {
this.errMsg = this.errMsg + '\nPlease set config path Or api password for node index ' + node.index + ' in RTL-Config.json! It is mandatory for Eclair authentication!'; this.errMsg = this.errMsg + '\nPlease set config path Or api password for node index ' + node.index + ' in RTL-Config.json! It is mandatory for Eclair authentication!';
} }
if (process.env.LN_SERVER_URL && process.env.LN_SERVER_URL.trim() !== '') { if (process?.env?.LN_SERVER_URL && process?.env?.LN_SERVER_URL.trim() !== '') {
this.common.nodes[idx].ln_server_url = process.env.LN_SERVER_URL.endsWith('/v1') ? process.env.LN_SERVER_URL.slice(0, -3) : process.env.LN_SERVER_URL; this.common.nodes[idx].ln_server_url = process?.env?.LN_SERVER_URL.endsWith('/v1') ? process?.env?.LN_SERVER_URL.slice(0, -3) : process?.env?.LN_SERVER_URL;
} else if (process.env.LND_SERVER_URL && process.env.LND_SERVER_URL.trim() !== '') { } else if (process?.env?.LND_SERVER_URL && process?.env?.LND_SERVER_URL.trim() !== '') {
this.common.nodes[idx].ln_server_url = process.env.LND_SERVER_URL.endsWith('/v1') ? process.env.LND_SERVER_URL.slice(0, -3) : process.env.LND_SERVER_URL; this.common.nodes[idx].ln_server_url = process?.env?.LND_SERVER_URL.endsWith('/v1') ? process?.env?.LND_SERVER_URL.slice(0, -3) : process?.env?.LND_SERVER_URL;
} else if (node.Settings.lnServerUrl && node.Settings.lnServerUrl.trim() !== '') { } else if (node.Settings.lnServerUrl && node.Settings.lnServerUrl.trim() !== '') {
this.common.nodes[idx].ln_server_url = node.Settings.lnServerUrl.endsWith('/v1') ? node.Settings.lnServerUrl.slice(0, -3) : node.Settings.lnServerUrl; this.common.nodes[idx].ln_server_url = node.Settings.lnServerUrl.endsWith('/v1') ? node.Settings.lnServerUrl.slice(0, -3) : node.Settings.lnServerUrl;
} else if (node.Settings.lndServerUrl && node.Settings.lndServerUrl.trim() !== '') { } else if (node.Settings.lndServerUrl && node.Settings.lndServerUrl.trim() !== '') {
@ -207,9 +216,9 @@ export class ConfigService {
if (this.common.nodes[idx].fiat_conversion) { if (this.common.nodes[idx].fiat_conversion) {
this.common.nodes[idx].currency_unit = node.Settings.currencyUnit ? node.Settings.currencyUnit : 'USD'; this.common.nodes[idx].currency_unit = node.Settings.currencyUnit ? node.Settings.currencyUnit : 'USD';
} }
if (process.env.SWAP_SERVER_URL && process.env.SWAP_SERVER_URL.trim() !== '') { if (process?.env?.SWAP_SERVER_URL && process?.env?.SWAP_SERVER_URL.trim() !== '') {
this.common.nodes[idx].swap_server_url = process.env.SWAP_SERVER_URL.endsWith('/v1') ? process.env.SWAP_SERVER_URL.slice(0, -3) : process.env.SWAP_SERVER_URL; this.common.nodes[idx].swap_server_url = process?.env?.SWAP_SERVER_URL.endsWith('/v1') ? process?.env?.SWAP_SERVER_URL.slice(0, -3) : process?.env?.SWAP_SERVER_URL;
this.common.nodes[idx].swap_macaroon_path = process.env.SWAP_MACAROON_PATH; this.common.nodes[idx].swap_macaroon_path = process?.env?.SWAP_MACAROON_PATH;
} else if (node.Settings.swapServerUrl && node.Settings.swapServerUrl.trim() !== '') { } else if (node.Settings.swapServerUrl && node.Settings.swapServerUrl.trim() !== '') {
this.common.nodes[idx].swap_server_url = node.Settings.swapServerUrl.endsWith('/v1') ? node.Settings.swapServerUrl.slice(0, -3) : node.Settings.swapServerUrl; this.common.nodes[idx].swap_server_url = node.Settings.swapServerUrl.endsWith('/v1') ? node.Settings.swapServerUrl.slice(0, -3) : node.Settings.swapServerUrl;
this.common.nodes[idx].swap_macaroon_path = node.Authentication.swapMacaroonPath ? node.Authentication.swapMacaroonPath : ''; this.common.nodes[idx].swap_macaroon_path = node.Authentication.swapMacaroonPath ? node.Authentication.swapMacaroonPath : '';
@ -217,9 +226,9 @@ export class ConfigService {
this.common.nodes[idx].swap_server_url = ''; this.common.nodes[idx].swap_server_url = '';
this.common.nodes[idx].swap_macaroon_path = ''; this.common.nodes[idx].swap_macaroon_path = '';
} }
if (process.env.BOLTZ_SERVER_URL && process.env.BOLTZ_SERVER_URL.trim() !== '') { if (process?.env?.BOLTZ_SERVER_URL && process?.env?.BOLTZ_SERVER_URL.trim() !== '') {
this.common.nodes[idx].boltz_server_url = process.env.BOLTZ_SERVER_URL.endsWith('/v1') ? process.env.BOLTZ_SERVER_URL.slice(0, -3) : process.env.BOLTZ_SERVER_URL; this.common.nodes[idx].boltz_server_url = process?.env?.BOLTZ_SERVER_URL.endsWith('/v1') ? process?.env?.BOLTZ_SERVER_URL.slice(0, -3) : process?.env?.BOLTZ_SERVER_URL;
this.common.nodes[idx].boltz_macaroon_path = process.env.BOLTZ_MACAROON_PATH; this.common.nodes[idx].boltz_macaroon_path = process?.env?.BOLTZ_MACAROON_PATH;
} else if (node.Settings.boltzServerUrl && node.Settings.boltzServerUrl.trim() !== '') { } else if (node.Settings.boltzServerUrl && node.Settings.boltzServerUrl.trim() !== '') {
this.common.nodes[idx].boltz_server_url = node.Settings.boltzServerUrl.endsWith('/v1') ? node.Settings.boltzServerUrl.slice(0, -3) : node.Settings.boltzServerUrl; this.common.nodes[idx].boltz_server_url = node.Settings.boltzServerUrl.endsWith('/v1') ? node.Settings.boltzServerUrl.slice(0, -3) : node.Settings.boltzServerUrl;
this.common.nodes[idx].boltz_macaroon_path = node.Authentication.boltzMacaroonPath ? node.Authentication.boltzMacaroonPath : ''; this.common.nodes[idx].boltz_macaroon_path = node.Authentication.boltzMacaroonPath ? node.Authentication.boltzMacaroonPath : '';
@ -227,10 +236,10 @@ export class ConfigService {
this.common.nodes[idx].boltz_server_url = ''; this.common.nodes[idx].boltz_server_url = '';
this.common.nodes[idx].boltz_macaroon_path = ''; this.common.nodes[idx].boltz_macaroon_path = '';
} }
this.common.nodes[idx].enable_offers = process.env.ENABLE_OFFERS ? process.env.ENABLE_OFFERS : (node.Settings.enableOffers) ? node.Settings.enableOffers : false; this.common.nodes[idx].enable_offers = process?.env?.ENABLE_OFFERS ? process?.env?.ENABLE_OFFERS : (node.Settings.enableOffers) ? node.Settings.enableOffers : false;
this.common.nodes[idx].enable_peerswap = process.env.ENABLE_PEERSWAP ? process.env.ENABLE_PEERSWAP : (node.Settings.enablePeerswap) ? node.Settings.enablePeerswap : false; this.common.nodes[idx].enable_peerswap = process?.env?.ENABLE_PEERSWAP ? process?.env?.ENABLE_PEERSWAP : (node.Settings.enablePeerswap) ? node.Settings.enablePeerswap : false;
this.common.nodes[idx].bitcoind_config_path = process.env.BITCOIND_CONFIG_PATH ? process.env.BITCOIND_CONFIG_PATH : (node.Settings.bitcoindConfigPath) ? node.Settings.bitcoindConfigPath : ''; this.common.nodes[idx].bitcoind_config_path = process?.env?.BITCOIND_CONFIG_PATH ? process?.env?.BITCOIND_CONFIG_PATH : (node.Settings.bitcoindConfigPath) ? node.Settings.bitcoindConfigPath : '';
this.common.nodes[idx].channel_backup_path = process.env.CHANNEL_BACKUP_PATH ? process.env.CHANNEL_BACKUP_PATH : (node.Settings.channelBackupPath) ? node.Settings.channelBackupPath : this.common.rtl_conf_file_path + sep + 'channels-backup' + sep + 'node-' + node.index; this.common.nodes[idx].channel_backup_path = process?.env?.CHANNEL_BACKUP_PATH ? process?.env?.CHANNEL_BACKUP_PATH : (node.Settings.channelBackupPath) ? node.Settings.channelBackupPath : this.common.rtl_conf_file_path + sep + 'channels-backup' + sep + 'node-' + node.index;
try { try {
this.common.createDirectory(this.common.nodes[idx].channel_backup_path); this.common.createDirectory(this.common.nodes[idx].channel_backup_path);
const exists = fs.existsSync(this.common.nodes[idx].channel_backup_path + sep + 'channel-all.bak'); const exists = fs.existsSync(this.common.nodes[idx].channel_backup_path + sep + 'channel-all.bak');
@ -252,13 +261,13 @@ export class ConfigService {
this.common.nodes[idx].log_file = this.common.rtl_conf_file_path + '/logs/RTL-Node-' + node.index + '.log'; this.common.nodes[idx].log_file = this.common.rtl_conf_file_path + '/logs/RTL-Node-' + node.index + '.log';
this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'Config', msg: 'Node Config: ' + JSON.stringify(this.common.nodes[idx]) }); this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'INFO', fileName: 'Config', msg: 'Node Config: ' + JSON.stringify(this.common.nodes[idx]) });
const log_file = this.common.nodes[idx].log_file; const log_file = this.common.nodes[idx].log_file;
if (fs.existsSync(log_file)) { if (fs.existsSync(log_file || '')) {
fs.writeFile(log_file, '', () => { }); fs.writeFile((log_file || ''), '', () => { });
} else { } else {
try { try {
const directoryName = dirname(log_file); const directoryName = dirname(log_file || '');
this.common.createDirectory(directoryName); this.common.createDirectory(directoryName);
const createStream = fs.createWriteStream(log_file); const createStream = fs.createWriteStream(log_file || '');
createStream.end(); createStream.end();
} catch (err) { } catch (err) {
this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while creating log file ' + log_file + ': \n' + err }); this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while creating log file ' + log_file + ': \n' + err });
@ -271,22 +280,22 @@ export class ConfigService {
}; };
private setSSOParams = (config) => { private setSSOParams = (config) => {
if (process.env.RTL_SSO) { if (process?.env?.RTL_SSO) {
this.common.rtl_sso = +process.env.RTL_SSO; this.common.rtl_sso = +process?.env?.RTL_SSO;
} else if (config.SSO && config.SSO.rtlSSO) { } else if (config.SSO && config.SSO.rtlSSO) {
this.common.rtl_sso = config.SSO.rtlSSO; this.common.rtl_sso = config.SSO.rtlSSO;
} }
if (process.env.RTL_COOKIE_PATH) { if (process?.env?.RTL_COOKIE_PATH) {
this.common.rtl_cookie_path = process.env.RTL_COOKIE_PATH; this.common.rtl_cookie_path = process?.env?.RTL_COOKIE_PATH;
} else if (config.SSO && config.SSO.rtlCookiePath) { } else if (config.SSO && config.SSO.rtlCookiePath) {
this.common.rtl_cookie_path = config.SSO.rtlCookiePath; this.common.rtl_cookie_path = config.SSO.rtlCookiePath;
} else { } else {
this.common.rtl_cookie_path = ''; this.common.rtl_cookie_path = '';
} }
if (process.env.LOGOUT_REDIRECT_LINK) { if (process?.env?.LOGOUT_REDIRECT_LINK) {
this.common.logout_redirect_link = process.env.LOGOUT_REDIRECT_LINK; this.common.logout_redirect_link = process?.env?.LOGOUT_REDIRECT_LINK;
} else if (config.SSO && config.SSO.logoutRedirectLink) { } else if (config.SSO && config.SSO.logoutRedirectLink) {
this.common.logout_redirect_link = config.SSO.logoutRedirectLink; this.common.logout_redirect_link = config.SSO.logoutRedirectLink;
} }
@ -302,15 +311,15 @@ export class ConfigService {
private setSelectedNode = (config) => { private setSelectedNode = (config) => {
if (config.defaultNodeIndex) { if (config.defaultNodeIndex) {
this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex); this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex) || {};
} else { } else {
this.common.initSelectedNode = this.common.findNode(this.common.nodes[0].index); this.common.initSelectedNode = this.common.findNode(this.common.nodes[0].index) || {};
} }
}; };
public setServerConfiguration = () => { public setServerConfiguration = () => {
try { try {
this.common.rtl_conf_file_path = (process.env.RTL_CONFIG_PATH) ? process.env.RTL_CONFIG_PATH : join(this.directoryName, '../..'); this.common.rtl_conf_file_path = (process?.env?.RTL_CONFIG_PATH) ? process?.env?.RTL_CONFIG_PATH : join(this.directoryName, '../..');
const confFileFullPath = this.common.rtl_conf_file_path + sep + 'RTL-Config.json'; const confFileFullPath = this.common.rtl_conf_file_path + sep + 'RTL-Config.json';
if (!fs.existsSync(confFileFullPath)) { if (!fs.existsSync(confFileFullPath)) {
fs.writeFileSync(confFileFullPath, JSON.stringify(this.setDefaultConfig())); fs.writeFileSync(confFileFullPath, JSON.stringify(this.setDefaultConfig()));
@ -319,7 +328,7 @@ export class ConfigService {
this.updateLogByLevel(); this.updateLogByLevel();
this.validateNodeConfig(config); this.validateNodeConfig(config);
this.setSelectedNode(config); this.setSelectedNode(config);
} catch (err) { } catch (err: any) {
this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while configuring the node server: \n' + err }); this.logger.log({ selectedNode: this.common.initSelectedNode, level: 'ERROR', fileName: 'Config', msg: 'Something went wrong while configuring the node server: \n' + err });
throw new Error(err); throw new Error(err);
} }

@ -1,6 +1,5 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { join, dirname, sep } from 'path'; import { join, sep } from 'path';
import { fileURLToPath } from 'url';
import { Common, CommonService } from '../utils/common.js'; import { Common, CommonService } from '../utils/common.js';
import { Logger, LoggerService } from '../utils/logger.js'; import { Logger, LoggerService } from '../utils/logger.js';
import { Collections, CollectionsEnum, validateDocument, LNDCollection, ECLCollection, CLNCollection } from '../models/database.model.js'; import { Collections, CollectionsEnum, validateDocument, LNDCollection, ECLCollection, CLNCollection } from '../models/database.model.js';
@ -10,7 +9,7 @@ export class DatabaseService {
public common: CommonService = Common; public common: CommonService = Common;
public logger: LoggerService = Logger; public logger: LoggerService = Logger;
public dbDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'database'); public dbDirectory = join(this.common.db_directory_path, 'database');
public nodeDatabase: { id?: { adapter: DatabaseAdapter, data: Collections } } = {}; public nodeDatabase: { id?: { adapter: DatabaseAdapter, data: Collections } } = {};
constructor() { } constructor() { }

@ -30,6 +30,7 @@ import { RootReducer } from './store/rtl.reducers';
import { LNDReducer } from './lnd/store/lnd.reducers'; import { LNDReducer } from './lnd/store/lnd.reducers';
import { CLNReducer } from './cln/store/cln.reducers'; import { CLNReducer } from './cln/store/cln.reducers';
import { ECLReducer } from './eclair/store/ecl.reducers'; import { ECLReducer } from './eclair/store/ecl.reducers';
import { HOUR_SECONDS } from './shared/services/consts-enums-functions';
let isDevEnvironemt = false; let isDevEnvironemt = false;
if (isDevMode()) { isDevEnvironemt = true; } if (isDevMode()) { isDevEnvironemt = true; }
@ -41,7 +42,7 @@ if (isDevMode()) { isDevEnvironemt = true; }
routing, routing,
LayoutModule, LayoutModule,
HammerModule, HammerModule,
UserIdleModule.forRoot({ idle: 3590, timeout: 10, ping: 12000 }), // One hour => 3590 + 10 = 3600 UserIdleModule.forRoot({ idle: (HOUR_SECONDS - 10), timeout: 10, ping: 12000 }),
StoreModule.forRoot( StoreModule.forRoot(
{ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer }, { root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer },
{ {
@ -56,7 +57,7 @@ if (isDevMode()) { isDevEnvironemt = true; }
declarations: [AppComponent], declarations: [AppComponent],
providers: [ providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
AuthGuard, SessionService, DataService, WebSocketClientService, LoopService, CommonService, BoltzService SessionService, DataService, WebSocketClientService, LoopService, CommonService, BoltzService
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

@ -26,35 +26,35 @@ type PathMatch = 'full' | 'prefix' | undefined;
export const routes: Routes = [ export const routes: Routes = [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'login' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'login' },
{ path: 'lnd', loadChildren: () => import('./lnd/lnd.module').then((childModule) => childModule.LNDModule), canActivate: [AuthGuard] }, { path: 'lnd', loadChildren: () => import('./lnd/lnd.module').then((childModule) => childModule.LNDModule), canActivate: [AuthGuard()] },
{ path: 'cln', loadChildren: () => import('./cln/cln.module').then((childModule) => childModule.CLNModule), canActivate: [AuthGuard] }, { path: 'cln', loadChildren: () => import('./cln/cln.module').then((childModule) => childModule.CLNModule), canActivate: [AuthGuard()] },
{ path: 'ecl', loadChildren: () => import('./eclair/ecl.module').then((childModule) => childModule.ECLModule), canActivate: [AuthGuard] }, { path: 'ecl', loadChildren: () => import('./eclair/ecl.module').then((childModule) => childModule.ECLModule), canActivate: [AuthGuard()] },
{ {
path: 'settings', component: SettingsComponent, canActivate: [AuthGuard], children: [ path: 'settings', component: SettingsComponent, canActivate: [AuthGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'app' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'app' },
{ path: 'app', component: AppSettingsComponent, canActivate: [AuthGuard] }, { path: 'app', component: AppSettingsComponent, canActivate: [AuthGuard()] },
{ path: 'auth', component: AuthSettingsComponent, canActivate: [AuthGuard] }, { path: 'auth', component: AuthSettingsComponent, canActivate: [AuthGuard()] },
{ path: 'bconfig', component: BitcoinConfigComponent, canActivate: [AuthGuard] } { path: 'bconfig', component: BitcoinConfigComponent, canActivate: [AuthGuard()] }
] ]
}, },
{ {
path: 'config', component: NodeConfigComponent, canActivate: [AuthGuard], children: [ path: 'config', component: NodeConfigComponent, canActivate: [AuthGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'nodesettings' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'nodesettings' },
{ path: 'nodesettings', component: NodeSettingsComponent, canActivate: [AuthGuard] }, { path: 'nodesettings', component: NodeSettingsComponent, canActivate: [AuthGuard()] },
{ path: 'pglayout', component: PageSettingsComponent, canActivate: [AuthGuard] }, { path: 'pglayout', component: PageSettingsComponent, canActivate: [AuthGuard()] },
{ {
path: 'services', component: ServicesSettingsComponent, canActivate: [AuthGuard], children: [ path: 'services', component: ServicesSettingsComponent, canActivate: [AuthGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'loop' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'loop' },
{ path: 'loop', component: LoopServiceSettingsComponent, canActivate: [AuthGuard] }, { path: 'loop', component: LoopServiceSettingsComponent, canActivate: [AuthGuard()] },
{ path: 'boltz', component: BoltzServiceSettingsComponent, canActivate: [AuthGuard] } { path: 'boltz', component: BoltzServiceSettingsComponent, canActivate: [AuthGuard()] }
] ]
}, },
{ path: 'experimental', component: ExperimentalSettingsComponent, canActivate: [AuthGuard] }, { path: 'experimental', component: ExperimentalSettingsComponent, canActivate: [AuthGuard()] },
{ path: 'lnconfig', component: LNPConfigComponent, canActivate: [AuthGuard] } { path: 'lnconfig', component: LNPConfigComponent, canActivate: [AuthGuard()] }
] ]
}, },
{ {
path: 'services', component: LNServicesComponent, canActivate: [AuthGuard], children: [ path: 'services', component: LNServicesComponent, canActivate: [AuthGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'loop' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'loop' },
{ path: 'loop', pathMatch: <PathMatch>'full', redirectTo: 'loop/loopout' }, { path: 'loop', pathMatch: <PathMatch>'full', redirectTo: 'loop/loopout' },
{ path: 'loop/:selTab', component: LoopComponent }, { path: 'loop/:selTab', component: LoopComponent },
@ -69,4 +69,4 @@ export const routes: Routes = [
]; ];
// Export const routing: ModuleWithProviders<RouterModule> = RouterModule.forRoot(routes, { enableTracing: true }); // Export const routing: ModuleWithProviders<RouterModule> = RouterModule.forRoot(routes, { enableTracing: true });
export const routing: ModuleWithProviders<RouterModule> = RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled' }); export const routing: ModuleWithProviders<RouterModule> = RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload', scrollPositionRestoration: 'enabled' });

@ -58,6 +58,7 @@ import { CLNOffersTableComponent } from './transactions/offers/offers-table/offe
import { CLNOfferBookmarksTableComponent } from './transactions/offers/offer-bookmarks-table/offer-bookmarks-table.component'; import { CLNOfferBookmarksTableComponent } from './transactions/offers/offer-bookmarks-table/offer-bookmarks-table.component';
import { CLNLiquidityAdsListComponent } from './liquidity-ads/liquidity-ads-list/liquidity-ads-list.component'; import { CLNLiquidityAdsListComponent } from './liquidity-ads/liquidity-ads-list/liquidity-ads-list.component';
import { CLNOpenLiquidityChannelComponent } from './liquidity-ads/open-liquidity-channel-modal/open-liquidity-channel-modal.component'; import { CLNOpenLiquidityChannelComponent } from './liquidity-ads/open-liquidity-channel-modal/open-liquidity-channel-modal.component';
import { CLNChannelActiveHTLCsTableComponent } from './peers-channels/channels/channels-tables/channel-active-htlcs-table/channel-active-htlcs-table.component';
import { CLNUnlockedGuard } from '../shared/services/auth.guard'; import { CLNUnlockedGuard } from '../shared/services/auth.guard';
@ -121,11 +122,10 @@ import { CLNUnlockedGuard } from '../shared/services/auth.guard';
CLNOffersTableComponent, CLNOffersTableComponent,
CLNOfferBookmarksTableComponent, CLNOfferBookmarksTableComponent,
CLNLiquidityAdsListComponent, CLNLiquidityAdsListComponent,
CLNOpenLiquidityChannelComponent CLNOpenLiquidityChannelComponent,
], CLNChannelActiveHTLCsTableComponent
providers: [
CLNUnlockedGuard
], ],
providers: [],
bootstrap: [CLNRootComponent] bootstrap: [CLNRootComponent]
}) })
export class CLNModule { } export class CLNModule { }

@ -24,6 +24,7 @@ import { CLNVerifyComponent } from './sign-verify-message/verify/verify.componen
import { CLNForwardingHistoryComponent } from './routing/forwarding-history/forwarding-history.component'; import { CLNForwardingHistoryComponent } from './routing/forwarding-history/forwarding-history.component';
import { CLNFailedTransactionsComponent } from './routing/failed-transactions/failed-transactions.component'; import { CLNFailedTransactionsComponent } from './routing/failed-transactions/failed-transactions.component';
import { CLNRoutingPeersComponent } from './routing/routing-peers/routing-peers.component'; import { CLNRoutingPeersComponent } from './routing/routing-peers/routing-peers.component';
import { CLNChannelActiveHTLCsTableComponent } from './peers-channels/channels/channels-tables/channel-active-htlcs-table/channel-active-htlcs-table.component';
import { CLNReportsComponent } from './reports/reports.component'; import { CLNReportsComponent } from './reports/reports.component';
import { CLNRoutingReportComponent } from './reports/routing/routing-report.component'; import { CLNRoutingReportComponent } from './reports/routing/routing-report.component';
@ -43,69 +44,70 @@ export const ClnRoutes: Routes = [
path: '', component: CLNRootComponent, path: '', component: CLNRootComponent,
children: [ children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'home' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'home' },
{ path: 'home', component: CLNHomeComponent, canActivate: [CLNUnlockedGuard] }, { path: 'home', component: CLNHomeComponent, canActivate: [CLNUnlockedGuard()] },
{ {
path: 'onchain', component: CLNOnChainComponent, canActivate: [CLNUnlockedGuard], children: [ path: 'onchain', component: CLNOnChainComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'receive/utxos' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'receive/utxos' },
{ path: 'receive/:selTab', component: CLNOnChainReceiveComponent, canActivate: [CLNUnlockedGuard] }, { path: 'receive/:selTab', component: CLNOnChainReceiveComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'send/:selTab', component: CLNOnChainSendComponent, data: { sweepAll: false }, canActivate: [CLNUnlockedGuard] }, { path: 'send/:selTab', component: CLNOnChainSendComponent, data: { sweepAll: false }, canActivate: [CLNUnlockedGuard()] },
{ path: 'sweep/:selTab', component: CLNOnChainSendComponent, data: { sweepAll: true }, canActivate: [CLNUnlockedGuard] } { path: 'sweep/:selTab', component: CLNOnChainSendComponent, data: { sweepAll: true }, canActivate: [CLNUnlockedGuard()] }
] ]
}, },
{ {
path: 'connections', component: CLNConnectionsComponent, canActivate: [CLNUnlockedGuard], children: [ path: 'connections', component: CLNConnectionsComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'channels' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'channels' },
{ {
path: 'channels', component: CLNChannelsTablesComponent, canActivate: [CLNUnlockedGuard], children: [ path: 'channels', component: CLNChannelsTablesComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'open' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'open' },
{ path: 'open', component: CLNChannelOpenTableComponent, canActivate: [CLNUnlockedGuard] }, { path: 'open', component: CLNChannelOpenTableComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'pending', component: CLNChannelPendingTableComponent, canActivate: [CLNUnlockedGuard] } { path: 'pending', component: CLNChannelPendingTableComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'activehtlcs', component: CLNChannelActiveHTLCsTableComponent, canActivate: [CLNUnlockedGuard()] }
] ]
}, },
{ path: 'peers', component: CLNPeersComponent, data: { sweepAll: false }, canActivate: [CLNUnlockedGuard] } { path: 'peers', component: CLNPeersComponent, data: { sweepAll: false }, canActivate: [CLNUnlockedGuard()] }
] ]
}, },
{ path: 'liquidityads', component: CLNLiquidityAdsListComponent, canActivate: [CLNUnlockedGuard] }, { path: 'liquidityads', component: CLNLiquidityAdsListComponent, canActivate: [CLNUnlockedGuard()] },
{ {
path: 'transactions', component: CLNTransactionsComponent, canActivate: [CLNUnlockedGuard], children: [ path: 'transactions', component: CLNTransactionsComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'payments' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'payments' },
{ path: 'payments', component: CLNLightningPaymentsComponent, canActivate: [CLNUnlockedGuard] }, { path: 'payments', component: CLNLightningPaymentsComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'invoices', component: CLNLightningInvoicesTableComponent, canActivate: [CLNUnlockedGuard] }, { path: 'invoices', component: CLNLightningInvoicesTableComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'offers', component: CLNOffersTableComponent, canActivate: [CLNUnlockedGuard] }, { path: 'offers', component: CLNOffersTableComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'offrBookmarks', component: CLNOfferBookmarksTableComponent, canActivate: [CLNUnlockedGuard] } { path: 'offrBookmarks', component: CLNOfferBookmarksTableComponent, canActivate: [CLNUnlockedGuard()] }
] ]
}, },
{ {
path: 'messages', component: CLNSignVerifyMessageComponent, canActivate: [CLNUnlockedGuard], children: [ path: 'messages', component: CLNSignVerifyMessageComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'sign' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'sign' },
{ path: 'sign', component: CLNSignComponent, canActivate: [CLNUnlockedGuard] }, { path: 'sign', component: CLNSignComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'verify', component: CLNVerifyComponent, canActivate: [CLNUnlockedGuard] } { path: 'verify', component: CLNVerifyComponent, canActivate: [CLNUnlockedGuard()] }
] ]
}, },
{ {
path: 'routing', component: CLNRoutingComponent, canActivate: [CLNUnlockedGuard], children: [ path: 'routing', component: CLNRoutingComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'forwardinghistory' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'forwardinghistory' },
{ path: 'forwardinghistory', component: CLNForwardingHistoryComponent, canActivate: [CLNUnlockedGuard] }, { path: 'forwardinghistory', component: CLNForwardingHistoryComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'failedtransactions', component: CLNFailedTransactionsComponent, canActivate: [CLNUnlockedGuard] }, { path: 'failedtransactions', component: CLNFailedTransactionsComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'localfail', component: CLNLocalFailedTransactionsComponent, canActivate: [CLNUnlockedGuard] }, { path: 'localfail', component: CLNLocalFailedTransactionsComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'routingpeers', component: CLNRoutingPeersComponent, canActivate: [CLNUnlockedGuard] } { path: 'routingpeers', component: CLNRoutingPeersComponent, canActivate: [CLNUnlockedGuard()] }
] ]
}, },
{ {
path: 'reports', component: CLNReportsComponent, canActivate: [CLNUnlockedGuard], children: [ path: 'reports', component: CLNReportsComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'routingreport' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'routingreport' },
{ path: 'routingreport', component: CLNRoutingReportComponent, canActivate: [CLNUnlockedGuard] }, { path: 'routingreport', component: CLNRoutingReportComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'transactions', component: CLNTransactionsReportComponent, canActivate: [CLNUnlockedGuard] } { path: 'transactions', component: CLNTransactionsReportComponent, canActivate: [CLNUnlockedGuard()] }
] ]
}, },
{ {
path: 'graph', component: CLNGraphComponent, canActivate: [CLNUnlockedGuard], children: [ path: 'graph', component: CLNGraphComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'lookups' }, { path: '', pathMatch: <PathMatch>'full', redirectTo: 'lookups' },
{ path: 'lookups', component: CLNLookupsComponent, canActivate: [CLNUnlockedGuard] }, { path: 'lookups', component: CLNLookupsComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'queryroutes', component: CLNQueryRoutesComponent, canActivate: [CLNUnlockedGuard] } { path: 'queryroutes', component: CLNQueryRoutesComponent, canActivate: [CLNUnlockedGuard()] }
] ]
}, },
{ path: 'rates', component: CLNNetworkInfoComponent, canActivate: [CLNUnlockedGuard] }, { path: 'rates', component: CLNNetworkInfoComponent, canActivate: [CLNUnlockedGuard()] },
{ path: '**', component: NotFoundComponent }, { path: '**', component: NotFoundComponent },
{ path: 'network', redirectTo: 'rates' }, { path: 'network', redirectTo: 'rates' },
{ path: 'wallet', redirectTo: 'home' }, { path: 'wallet', redirectTo: 'home' },

@ -19,12 +19,12 @@
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Last Update</h4> <h4 class="font-bold-500">Last Update</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.last_update }}</span> <span class="foreground-secondary-text">{{(lookupResult[0]?.last_update * 1000) | date:'dd/MMM/y HH:mm'}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Amount (mSats)</h4> <h4 class="font-bold-500">Amount (Sats)</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.amount_msat}}</span> <span class="foreground-secondary-text">{{lookupResult[0]?.amount_msat / 1000 | number:'1.0-0'}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
@ -32,6 +32,11 @@
<span class="foreground-secondary-text">{{lookupResult[0]?.base_fee_millisatoshi | number}}</span> <span class="foreground-secondary-text">{{lookupResult[0]?.base_fee_millisatoshi | number}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Fee/Millionth</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.fee_per_millionth | number}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Channel Flags</h4> <h4 class="font-bold-500">Channel Flags</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.channel_flags | number}}</span> <span class="foreground-secondary-text">{{lookupResult[0]?.channel_flags | number}}</span>
@ -42,24 +47,14 @@
<span class="foreground-secondary-text">{{lookupResult[0]?.delay | number}}</span> <span class="foreground-secondary-text">{{lookupResult[0]?.delay | number}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Destination</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.destination}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Fee/Millionth</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.fee_per_millionth | number}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Max Htlc (mSat)</h4> <h4 class="font-bold-500">Max Htlc (mSat)</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.htlc_maximum_msat}}</span> <span class="foreground-secondary-text">{{lookupResult[0]?.htlc_maximum_msat | number}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Min Htlc (mSat)</h4> <h4 class="font-bold-500">Min Htlc (mSat)</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.htlc_minimum_msat}}</span> <span class="foreground-secondary-text">{{lookupResult[0]?.htlc_minimum_msat | number}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
@ -73,13 +68,13 @@
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Satoshis</h4> <h4 class="font-bold-500">Source</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.satoshis | number}}</span> <span class="foreground-secondary-text">{{lookupResult[0]?.source}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Source</h4> <h4 class="font-bold-500">Destination</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.source}}</span> <span class="foreground-secondary-text">{{lookupResult[0]?.destination}}</span>
</div> </div>
</div> </div>
<div fxLayout="column" fxFlex="49" fxLayoutAlign="start stretch" class="mt-1 bordered-box padding-gap-large"> <div fxLayout="column" fxFlex="49" fxLayoutAlign="start stretch" class="mt-1 bordered-box padding-gap-large">
@ -104,8 +99,8 @@
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Amount (mSats)</h4> <h4 class="font-bold-500">Amount (Sats)</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.amount_msat}}</span> <span class="foreground-secondary-text">{{lookupResult[1]?.amount_msat / 1000 | number:'1.0-0'}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
@ -113,6 +108,11 @@
<span class="foreground-secondary-text">{{lookupResult[1]?.base_fee_millisatoshi | number}}</span> <span class="foreground-secondary-text">{{lookupResult[1]?.base_fee_millisatoshi | number}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Fee/Millionth</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.fee_per_millionth | number}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Channel Flags</h4> <h4 class="font-bold-500">Channel Flags</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.channel_flags | number}}</span> <span class="foreground-secondary-text">{{lookupResult[1]?.channel_flags | number}}</span>
@ -123,24 +123,14 @@
<span class="foreground-secondary-text">{{lookupResult[1]?.delay | number}}</span> <span class="foreground-secondary-text">{{lookupResult[1]?.delay | number}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Destination</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.destination}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Fee/Millionth</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.fee_per_millionth | number}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Max Htlc (mSat)</h4> <h4 class="font-bold-500">Max Htlc (mSat)</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.htlc_maximum_msat}}</span> <span class="foreground-secondary-text">{{lookupResult[1]?.htlc_maximum_msat | number}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Min Htlc (mSat)</h4> <h4 class="font-bold-500">Min Htlc (mSat)</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.htlc_minimum_msat}}</span> <span class="foreground-secondary-text">{{lookupResult[1]?.htlc_minimum_msat | number}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
@ -154,13 +144,13 @@
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Satoshis</h4> <h4 class="font-bold-500">Source</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.satoshis | number}}</span> <span class="foreground-secondary-text">{{lookupResult[1]?.source}}</span>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="column"> <div fxLayout="column">
<h4 class="font-bold-500">Source</h4> <h4 class="font-bold-500">Destination</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.source}}</span> <span class="foreground-secondary-text">{{lookupResult[1]?.destination}}</span>
</div> </div>
</div> </div>
</div> </div>

@ -34,38 +34,39 @@ export class CLNLookupsComponent implements OnInit, OnDestroy {
public faSearch = faSearch; public faSearch = faSearch;
public screenSize = ''; public screenSize = '';
public screenSizeEnum = ScreenSizeEnum; public screenSizeEnum = ScreenSizeEnum;
private unSubs: Array<Subject<void>> = [new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private actions: Actions) { constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private actions: Actions) {
this.screenSize = this.commonService.getScreenSize(); this.screenSize = this.commonService.getScreenSize();
} }
ngOnInit() { ngOnInit() {
this.actions. if (window.history.state && window.history.state.lookupType) {
pipe( this.selectedFieldId = +window.history.state.lookupType || 0;
takeUntil(this.unSubs[0]), this.lookupKey = window.history.state.lookupValue || '';
filter((action) => (action.type === CLNActions.SET_LOOKUP_CLN || action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN)) }
).subscribe((resLookup: any) => { this.actions.pipe(takeUntil(this.unSubs[0]), filter((action) => (action.type === CLNActions.SET_LOOKUP_CLN || action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN))
if (resLookup.type === CLNActions.SET_LOOKUP_CLN) { ).subscribe((resLookup: any) => {
this.flgLoading[0] = true; if (resLookup.type === CLNActions.SET_LOOKUP_CLN) {
switch (this.selectedFieldId) { this.flgLoading[0] = true;
case 0: switch (this.selectedFieldId) {
this.nodeLookupValue = typeof resLookup.payload[0] !== 'object' ? { nodeid: '' } : JSON.parse(JSON.stringify(resLookup.payload[0])); case 0:
break; this.nodeLookupValue = typeof resLookup.payload[0] !== 'object' ? { nodeid: '' } : JSON.parse(JSON.stringify(resLookup.payload[0]));
case 1: break;
this.channelLookupValue = typeof resLookup.payload[0] !== 'object' ? [] : JSON.parse(JSON.stringify(resLookup.payload)); case 1:
break; this.channelLookupValue = typeof resLookup.payload[0] !== 'object' ? [] : JSON.parse(JSON.stringify(resLookup.payload));
default: break;
break; default:
} break;
this.flgSetLookupValue = true;
this.logger.info(this.nodeLookupValue);
this.logger.info(this.channelLookupValue);
}
if (resLookup.type === CLNActions.UPDATE_API_CALL_STATUS_CLN && resLookup.payload.status === APICallStatusEnum.ERROR && resLookup.payload.action === 'Lookup') {
this.flgLoading[0] = 'error';
} }
}); this.flgSetLookupValue = true;
this.logger.info(this.nodeLookupValue);
this.logger.info(this.channelLookupValue);
}
if (resLookup.type === CLNActions.UPDATE_API_CALL_STATUS_CLN && resLookup.payload.status === APICallStatusEnum.ERROR && resLookup.payload.action === 'Lookup') {
this.flgLoading[0] = 'error';
}
});
} }
onLookup(): boolean | void { onLookup(): boolean | void {

@ -42,11 +42,15 @@
<th *matHeaderCellDef mat-header-cell> <th *matHeaderCellDef mat-header-cell>
<div class="bordered-box table-actions-select btn-action" fxLayoutAlign="center center">Actions</div> <div class="bordered-box table-actions-select btn-action" fxLayoutAlign="center center">Actions</div>
</th> </th>
<td *matCellDef="let address" mat-cell> <td *matCellDef="let address" mat-cell fxLayoutAlign="end center">
<span fxLayoutAlign="end center"> <div class="bordered-box table-actions-select" fxLayoutAlign="center center">
<button mat-stroked-button class="btn-action-copy" color="primary" type="button" tabindex="1" rtlClipboard [payload]="lookupResult?.nodeid + '@' + address.address + ':' + address.port" (copied)="onCopyNodeURI($event)">Copy Node URI</button> <mat-select placeholder="Actions" tabindex="1" class="mr-0">
</span> <mat-select-trigger></mat-select-trigger>
</td> <mat-option (click)="onConnectNode(address)">Connect</mat-option>
<mat-option rtlClipboard [payload]="lookupResult?.nodeid + '@' + address.address + ':' + address.port" (copied)="onCopyNodeURI($event)">Copy URI</mat-option>
</mat-select>
</div>
</td>
</ng-container> </ng-container>
<tr *matHeaderRowDef="displayedColumns;" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns;" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns;" mat-row></tr> <tr *matRowDef="let row; columns: displayedColumns;" mat-row></tr>

@ -1,4 +1,10 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { StoreModule } from '@ngrx/store';
import { RootReducer } from '../../../../store/rtl.reducers';
import { LNDReducer } from '../../../../lnd/store/lnd.reducers';
import { CLNReducer } from '../../../../cln/store/cln.reducers';
import { ECLReducer } from '../../../../eclair/store/ecl.reducers';
import { LoggerService } from '../../../../shared/services/logger.service'; import { LoggerService } from '../../../../shared/services/logger.service';
import { SharedModule } from '../../../../shared/shared.module'; import { SharedModule } from '../../../../shared/shared.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@ -14,7 +20,8 @@ describe('CLNNodeLookupComponent', () => {
declarations: [CLNNodeLookupComponent], declarations: [CLNNodeLookupComponent],
imports: [ imports: [
SharedModule, SharedModule,
BrowserAnimationsModule BrowserAnimationsModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer })
], ],
providers: [LoggerService] providers: [LoggerService]
}). }).

@ -1,26 +1,36 @@
import { Component, OnInit, Input, ViewChild } from '@angular/core'; import { Component, OnInit, Input, ViewChild, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { LookupNode } from '../../../../shared/models/clnModels'; import { Address, Balance, GetInfo, LookupNode } from '../../../../shared/models/clnModels';
import { NODE_FEATURES_CLN } from '../../../../shared/services/consts-enums-functions'; import { NODE_FEATURES_CLN } from '../../../../shared/services/consts-enums-functions';
import { LoggerService } from '../../../../shared/services/logger.service'; import { LoggerService } from '../../../../shared/services/logger.service';
import { RTLState } from '../../../../store/rtl.state';
import { openAlert } from '../../../../store/rtl.actions';
import { CLNConnectPeerComponent } from '../../../peers-channels/connect-peer/connect-peer.component';
import { nodeInfoAndBalance } from '../../../store/cln.selector';
@Component({ @Component({
selector: 'rtl-cln-node-lookup', selector: 'rtl-cln-node-lookup',
templateUrl: './node-lookup.component.html', templateUrl: './node-lookup.component.html',
styleUrls: ['./node-lookup.component.scss'] styleUrls: ['./node-lookup.component.scss']
}) })
export class CLNNodeLookupComponent implements OnInit { export class CLNNodeLookupComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined; @ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@Input() lookupResult: LookupNode; @Input() lookupResult: LookupNode;
public featureDescriptions: string[] = []; public featureDescriptions: string[] = [];
public addresses: any = new MatTableDataSource([]); public addresses: any = new MatTableDataSource([]);
public displayedColumns = ['type', 'address', 'port', 'actions']; public displayedColumns = ['type', 'address', 'port', 'actions'];
public information: GetInfo = {};
public availableBalance = 0;
private unSubs: Array<Subject<void>> = [new Subject()];
constructor(private logger: LoggerService, private snackBar: MatSnackBar) { } constructor(private logger: LoggerService, private snackBar: MatSnackBar, private store: Store<RTLState>) { }
ngOnInit() { ngOnInit() {
this.addresses = this.lookupResult && this.lookupResult.addresses ? new MatTableDataSource<any>([...this.lookupResult.addresses]) : new MatTableDataSource([]); this.addresses = this.lookupResult && this.lookupResult.addresses ? new MatTableDataSource<any>([...this.lookupResult.addresses]) : new MatTableDataSource([]);
@ -28,13 +38,32 @@ export class CLNNodeLookupComponent implements OnInit {
this.addresses.sort = this.sort; this.addresses.sort = this.sort;
this.addresses.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null); this.addresses.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null);
if (this.lookupResult.features && this.lookupResult.features.trim() !== '') { if (this.lookupResult.features && this.lookupResult.features.trim() !== '') {
this.lookupResult.features = this.lookupResult.features.substring(this.lookupResult.features.length - 40);
const featureHex = parseInt(this.lookupResult.features, 16); const featureHex = parseInt(this.lookupResult.features, 16);
NODE_FEATURES_CLN.forEach((feature) => { NODE_FEATURES_CLN.forEach((feature) => {
if (!!(featureHex & ((1 << feature.range.min) | (1 << feature.range.max)))) { if (featureHex & (1 << feature.range.min)) {
this.featureDescriptions.push(feature.description + '\n'); this.featureDescriptions.push('Mandatory: ' + feature.description + '\n');
} else if (featureHex & (1 << feature.range.max)) {
this.featureDescriptions.push('Optional: ' + feature.description + '\n');
} }
}); });
} }
this.store.select(nodeInfoAndBalance).pipe(takeUntil(this.unSubs[0])).
subscribe((infoBalSelector: { information: GetInfo, balance: Balance }) => {
this.information = infoBalSelector.information;
this.availableBalance = infoBalSelector.balance.totalBalance || 0;
});
}
onConnectNode(address: Address) {
this.store.dispatch(openAlert({
payload: {
data: {
message: { peer: { id: this.lookupResult.nodeid + '@' + address.address + ':' + address.port }, information: this.information, balance: this.availableBalance },
component: CLNConnectPeerComponent
}
}
}));
} }
onCopyNodeURI(payload: string) { onCopyNodeURI(payload: string) {
@ -42,4 +71,11 @@ export class CLNNodeLookupComponent implements OnInit {
this.logger.info('Copied Text: ' + payload); this.logger.info('Copied Text: ' + payload);
} }
ngOnDestroy() {
this.unSubs.forEach((completeSub) => {
completeSub.next(<any>null);
completeSub.complete();
});
}
} }

@ -58,7 +58,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="msatoshi"> <ng-container matColumnDef="msatoshi">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount (Sats)</th>
<td *matCellDef="let hop" mat-cell><span fxLayoutAlign="end center">{{hop?.msatoshi/1000 | number}}</span></td> <td *matCellDef="let hop" mat-cell><span fxLayoutAlign="end center">{{hop?.amount_msat/1000 | number}}</span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th *matHeaderCellDef mat-header-cell> <th *matHeaderCellDef mat-header-cell>

@ -90,10 +90,9 @@ export class CLNQueryRoutesComponent implements OnInit, OnDestroy {
[{ key: 'id', value: selHop.id, title: 'ID', width: 100, type: DataTypeEnum.STRING }], [{ key: 'id', value: selHop.id, title: 'ID', width: 100, type: DataTypeEnum.STRING }],
[{ key: 'channel', value: selHop.channel, title: 'Channel', width: 50, type: DataTypeEnum.STRING }, [{ key: 'channel', value: selHop.channel, title: 'Channel', width: 50, type: DataTypeEnum.STRING },
{ key: 'alias', value: selHop.alias, title: 'Peer Alias', width: 50, type: DataTypeEnum.STRING }], { key: 'alias', value: selHop.alias, title: 'Peer Alias', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'msatoshi', value: selHop.msatoshi, title: 'mSatoshi', width: 50, type: DataTypeEnum.NUMBER }, [{ key: 'amount_msat', value: selHop.amount_msat, title: 'Amount (mSat)', width: 34, type: DataTypeEnum.NUMBER },
{ key: 'amount_msat', value: selHop.amount_msat, title: 'Amount mSat', width: 50, type: DataTypeEnum.STRING }], { key: 'direction', value: selHop.direction, title: 'Direction', width: 33, type: DataTypeEnum.STRING },
[{ key: 'direction', value: selHop.direction, title: 'Direction', width: 50, type: DataTypeEnum.STRING }, { key: 'delay', value: selHop.delay, title: 'Delay', width: 33, type: DataTypeEnum.NUMBER }]
{ key: 'delay', value: selHop.delay, title: 'Delay', width: 50, type: DataTypeEnum.NUMBER }]
]; ];
this.store.dispatch(openAlert({ this.store.dispatch(openAlert({
payload: { payload: {

@ -1,17 +1,17 @@
<div *ngIf="errorMessage?.trim() === ''; else errorBlock" class="mt-1" fxLayout="column" fxFlex="100"fxLayoutAlign="space-between stretch"> <div *ngIf="errorMessage?.trim() === ''; else errorBlock" class="mt-1" fxLayout="column" fxFlex="100"fxLayoutAlign="space-between stretch">
<div> <div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Lightning</h4> <h4 fxLayoutAlign="start" class="dashboard-info-title">Lightning</h4>
<div class="overflow-wrap dashboard-info-value">{{balances.lightning | number}} Sats</div> <div class="overflow-wrap dashboard-info-value">{{balances.lightning | number:'1.0-0'}} Sats</div>
<mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{balances.lightning/balances.total*100}}"></mat-progress-bar> <mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{balances.lightning/balances.total*100}}"></mat-progress-bar>
</div> </div>
<div> <div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">On-chain</h4> <h4 fxLayoutAlign="start" class="dashboard-info-title">On-chain</h4>
<div class="overflow-wrap dashboard-info-value">{{balances.onchain | number}} Sats</div> <div class="overflow-wrap dashboard-info-value">{{balances.onchain | number:'1.0-0'}} Sats</div>
<mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{balances.onchain/balances.total*100}}"></mat-progress-bar> <mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{balances.onchain/balances.total*100}}"></mat-progress-bar>
</div> </div>
<div> <div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Total</h4> <h4 fxLayoutAlign="start" class="dashboard-info-title">Total</h4>
<div class="overflow-wrap dashboard-info-value">{{balances.total | number}} Sats</div> <div class="overflow-wrap dashboard-info-value">{{balances.total | number:'1.0-0'}} Sats</div>
</div> </div>
</div> </div>
<ng-template #errorBlock> <ng-template #errorBlock>

@ -15,18 +15,18 @@
<div class="channels-capacity-scroll" [perfectScrollbar]> <div class="channels-capacity-scroll" [perfectScrollbar]>
<div *ngIf="activeChannels && activeChannels.length > 0; else noChannelBlock" fxLayout="column"fxFlex="100"> <div *ngIf="activeChannels && activeChannels.length > 0; else noChannelBlock" fxLayout="column"fxFlex="100">
<div *ngFor="let channel of activeChannels" class="mt-2"> <div *ngFor="let channel of activeChannels" class="mt-2">
<a class="dashboard-capacity-header" [routerLink]="['../connections/channels/open']" [state]="{filter: channel.id}" matTooltip="{{channel.alias || channel.id}}" matTooltipDisabled="{{(channel.alias || channel.id).length < 26}}"> <a class="dashboard-capacity-header" [routerLink]="['../connections/channels/open']" [state]="{filterColumn: channel.alias ? 'alias' : 'peer_id', filterValue: channel.alias || channel.peer_id}" matTooltip="{{channel.alias || channel.peer_id}}" matTooltipDisabled="{{(channel.alias || channel.peer_id).length < 26}}">
{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}} {{(channel.alias || channel.peer_id) | slice:0:24}}{{(channel.alias || channel.peer_id).length > 25 ? '...' : ''}}
</a> </a>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100"> <div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint fxFlex="40" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Local:</strong>{{channel.msatoshi_to_us/1000 || 0 | number:'1.0-0'}} Sats</mat-hint> <mat-hint fxFlex="40" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Local:</strong>{{channel.to_us_msat/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint fxFlex="20" fxLayoutAlign="center center" class="font-size-90 color-primary"> <mat-hint fxFlex="20" fxLayoutAlign="center center" class="font-size-90 color-primary">
<fa-icon class="color-primary mr-3px" matTooltip="Balance Score" [icon]="faBalanceScale"></fa-icon> <fa-icon class="color-primary mr-3px" matTooltip="Balance Score" [icon]="faBalanceScale"></fa-icon>
({{channel.balancedness || 0 | number}}) ({{channel.balancedness || 0 | number}})
</mat-hint> </mat-hint>
<mat-hint fxFlex="40" fxLayoutAlign="end center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Remote:</strong>{{channel.msatoshi_to_them/1000 || 0 | number:'1.0-0'}} Sats</mat-hint> <mat-hint fxFlex="40" fxLayoutAlign="end center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Remote:</strong>{{channel.to_them_msat/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
</div> </div>
<mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{channel.msatoshi_to_us && channel.msatoshi_to_us > 0 ? ((+channel.msatoshi_to_us/((+channel.msatoshi_to_us)+(+channel.msatoshi_to_them)))*100) : 0}}"></mat-progress-bar> <mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{channel.to_us_msat && channel.to_us_msat > 0 ? ((channel.to_us_msat/((channel.to_us_msat)+(channel.to_them_msat)))*100) : 0}}"></mat-progress-bar>
</div> </div>
</div> </div>
</div> </div>

@ -8,15 +8,15 @@
<div fxLayout="column" fxFlex.gt-sm="88" fxFlex="84" fxLayoutAlign="start start" [perfectScrollbar]> <div fxLayout="column" fxFlex.gt-sm="88" fxFlex="84" fxLayoutAlign="start start" [perfectScrollbar]>
<div *ngIf="activeChannels && activeChannels.length > 0; else noChannelBlock" fxLayout="column" fxFlex="100"class="w-100"> <div *ngIf="activeChannels && activeChannels.length > 0; else noChannelBlock" fxLayout="column" fxFlex="100"class="w-100">
<div *ngFor="let channel of activeChannels" class="mt-2"> <div *ngFor="let channel of activeChannels" class="mt-2">
<a class="dashboard-capacity-header" [routerLink]="['../connections/channels/open']" [state]="{filter: channel.id}" matTooltip="{{channel.alias || channel.id}}" matTooltipDisabled="{{(channel.alias || channel.id).length < 26}}"> <a class="dashboard-capacity-header" [routerLink]="['../connections/channels/open']" [state]="{filterColumn: channel.alias ? 'alias' : 'peer_id', filterValue: channel.alias || channel.peer_id}" matTooltip="{{channel.alias || channel.peer_id}}" matTooltipDisabled="{{(channel.alias || channel.peer_id).length < 26}}">
{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}} {{(channel.alias || channel.peer_id) | slice:0:24}}{{(channel.alias || channel.peer_id).length > 25 ? '...' : ''}}
</a> </a>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100"> <div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint *ngIf="direction === 'In'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.msatoshi_to_them/1000 || 0 | number:'1.0-0'}} Sats</mat-hint> <mat-hint *ngIf="direction === 'In'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.to_them_msat/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint *ngIf="direction === 'Out'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.msatoshi_to_us/1000 || 0 | number:'1.0-0'}} Sats</mat-hint> <mat-hint *ngIf="direction === 'Out'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.to_us_msat/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
</div> </div>
<mat-progress-bar *ngIf="direction === 'In'" class="dashboard-progress-bar" mode="determinate" value="{{(totalLiquidity > 0) ? ((+channel.msatoshi_to_them/1000 || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar> <mat-progress-bar *ngIf="direction === 'In'" class="dashboard-progress-bar" mode="determinate" value="{{(totalLiquidity > 0) ? ((channel.to_them_msat/1000 || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar>
<mat-progress-bar *ngIf="direction === 'Out'" class="dashboard-progress-bar" mode="determinate" value="{{(totalLiquidity > 0) ? ((+channel.msatoshi_to_us/1000 || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar> <mat-progress-bar *ngIf="direction === 'Out'" class="dashboard-progress-bar" mode="determinate" value="{{(totalLiquidity > 0) ? ((channel.to_us_msat/1000 || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar>
</div> </div>
</div> </div>
</div> </div>

@ -16,15 +16,15 @@
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch"> <div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch">
<div> <div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4> <h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4>
<div class="overflow-wrap dashboard-info-value">{{(channelsStatus?.active?.capacity || 0) | number}} Sats</div> <div class="overflow-wrap dashboard-info-value">{{(channelsStatus?.active?.capacity || 0) | number:'1.0-0'}} Sats</div>
</div> </div>
<div> <div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4> <h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4>
<div class="overflow-wrap dashboard-info-value">{{(channelsStatus?.pending?.capacity || 0) | number}} Sats</div> <div class="overflow-wrap dashboard-info-value">{{(channelsStatus?.pending?.capacity || 0) | number:'1.0-0'}} Sats</div>
</div> </div>
<div> <div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4> <h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4>
<div class="overflow-wrap dashboard-info-value">{{(channelsStatus?.inactive?.capacity || 0) | number}} Sats</div> <div class="overflow-wrap dashboard-info-value">{{(channelsStatus?.inactive?.capacity || 0) | number:'1.0-0'}} Sats</div>
</div> </div>
</div> </div>
</div> </div>

@ -155,11 +155,11 @@ export class CLNHomeComponent implements OnInit, OnDestroy {
this.totalOutboundLiquidity = 0; this.totalOutboundLiquidity = 0;
this.activeChannels = channelsSeletor.activeChannels; this.activeChannels = channelsSeletor.activeChannels;
this.activeChannelsCapacity = JSON.parse(JSON.stringify(this.commonService.sortDescByKey(this.activeChannels, 'balancedness'))) || []; this.activeChannelsCapacity = JSON.parse(JSON.stringify(this.commonService.sortDescByKey(this.activeChannels, 'balancedness'))) || [];
this.allInboundChannels = JSON.parse(JSON.stringify(this.commonService.sortDescByKey(this.activeChannels?.filter((channel) => (channel.msatoshi_to_them ? channel.msatoshi_to_them > 0 : false)), 'msatoshi_to_them'))) || []; this.allInboundChannels = JSON.parse(JSON.stringify(this.commonService.sortDescByKey(this.activeChannels?.filter((channel) => (channel.to_them_msat ? channel.to_them_msat > 0 : false)), 'to_them_msat'))) || [];
this.allOutboundChannels = JSON.parse(JSON.stringify(this.commonService.sortDescByKey(this.activeChannels?.filter((channel) => (channel.msatoshi_to_us ? channel.msatoshi_to_us > 0 : false)), 'msatoshi_to_us'))) || []; this.allOutboundChannels = JSON.parse(JSON.stringify(this.commonService.sortDescByKey(this.activeChannels?.filter((channel) => (channel.to_us_msat ? channel.to_us_msat > 0 : false)), 'to_us_msat'))) || [];
this.activeChannels.forEach((channel) => { this.activeChannels.forEach((channel) => {
this.totalInboundLiquidity = this.totalInboundLiquidity + Math.ceil((channel.msatoshi_to_them || 0) / 1000); this.totalInboundLiquidity = this.totalInboundLiquidity + Math.ceil((channel.to_them_msat || 0) / 1000);
this.totalOutboundLiquidity = this.totalOutboundLiquidity + Math.floor((channel.msatoshi_to_us || 0) / 1000); this.totalOutboundLiquidity = this.totalOutboundLiquidity + Math.floor((channel.to_us_msat || 0) / 1000);
}); });
this.channelsStatus.active.channels = channelsSeletor.activeChannels.length || 0; this.channelsStatus.active.channels = channelsSeletor.activeChannels.length || 0;
this.channelsStatus.pending.channels = channelsSeletor.pendingChannels.length || 0; this.channelsStatus.pending.channels = channelsSeletor.pendingChannels.length || 0;
@ -205,8 +205,8 @@ export class CLNHomeComponent implements OnInit, OnDestroy {
if (this.sortField === 'Balance Score') { if (this.sortField === 'Balance Score') {
this.sortField = 'Capacity'; this.sortField = 'Capacity';
this.activeChannelsCapacity = this.activeChannels.sort((a, b) => { this.activeChannelsCapacity = this.activeChannels.sort((a, b) => {
const x = (a.msatoshi_to_us ? +a.msatoshi_to_us : 0) + (a.msatoshi_to_them ? +a.msatoshi_to_them : 0); const x = (a.to_us_msat ? +a.to_us_msat : 0) + (a.to_them_msat ? +a.to_them_msat : 0);
const y = (b.msatoshi_to_them ? +b.msatoshi_to_them : 0) + (b.msatoshi_to_them ? +b.msatoshi_to_them : 0); const y = (b.to_them_msat ? +b.to_them_msat : 0) + (b.to_them_msat ? +b.to_them_msat : 0);
return ((x > y) ? -1 : ((x < y) ? 1 : 0)); return ((x > y) ? -1 : ((x < y) ? 1 : 0));
}); });
} else { } else {

@ -141,7 +141,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
getLabel(column: string) { getLabel(column: string) {
const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column); const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column);
return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform(returnColumn.column, '_') : this.commonService.titleCase(column); return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform((returnColumn.column || ''), '_') : this.commonService.titleCase(column);
} }
setFilterPredicate() { setFilterPredicate() {
@ -230,8 +230,10 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
if (lqNode.features && lqNode.features.trim() !== '') { if (lqNode.features && lqNode.features.trim() !== '') {
const featureHex = parseInt(lqNode.features, 16); const featureHex = parseInt(lqNode.features, 16);
NODE_FEATURES_CLN.forEach((feature) => { NODE_FEATURES_CLN.forEach((feature) => {
if (!!(featureHex & ((1 << feature.range.min) | (1 << feature.range.max)))) { if (featureHex & (1 << feature.range.min)) {
featureDescriptions.push(feature.description); featureDescriptions.push('Mandatory: ' + feature.description);
} else if (featureHex & (1 << feature.range.max)) {
featureDescriptions.push('Optional: ' + feature.description);
} }
}); });
} }

@ -1,62 +1,92 @@
<div *ngIf="errorMessage?.trim() === ''; else errorBlock" fxLayout="column" fxLayout.gt-xs="row" fxFlex="100" fxLayoutAlign="stretch stretch"> <div *ngIf="errorMessage?.trim() === ''; else errorBlock" fxLayout="column" fxFlex="100" fxLayoutAlign="stretch stretch">
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2"> <div fxLayout="column" fxLayout.gt-xs="row" fxFlex="100" fxLayoutAlign="stretch stretch">
<div> <div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> <div>
Opening <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<mat-icon matTooltip="Default feerate for fundchannel and withdraw" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> Opening
</h4> <mat-icon matTooltip="Default feerate for fundchannel and withdraw" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.opening | number}}</div> </h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.opening | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Mutual Close
<mat-icon matTooltip="Feerate to aim for in cooperative shutdown. Note that since mutual close is a negotiation, the actual feerate used in mutual close will be somewhere between this and the corresponding mutual close feerate of the peer" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.mutual_close | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Unilateral Close
<mat-icon matTooltip="Feerate for commitment transaction in a live channel which we originally funded" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.unilateral_close | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Delayed To Us
<mat-icon matTooltip="Feerate for returning unilateral close funds to our wallet" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.delayed_to_us | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Minimum Acceptable
<mat-icon matTooltip="The smallest feerate that you can use, usually the minimum relayed feerate of the backend" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.min_acceptable | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Maximum Acceptable
<mat-icon matTooltip="The largest feerate we will accept from remote negotiations. If a peer attempts to set the feerate higher than this we will unilaterally close the channel (or simply forget it if it's not open yet)" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.max_acceptable | number}}</div>
</div>
</div> </div>
<div> <div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> <div>
Mutual Close <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<mat-icon matTooltip="Feerate to aim for in cooperative shutdown. Note that since mutual close is a negotiation, the actual feerate used in mutual close will be somewhere between this and the corresponding mutual close feerate of the peer" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> HTLC Resolution
</h4> <mat-icon matTooltip="Feerate for returning unilateral close HTLC outputs to our wallet" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.mutual_close | number}}</div> </h4>
</div> <div class="overflow-wrap dashboard-info-value">{{perkbw?.htlc_resolution | number}}</div>
<div> </div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> <div>
Unilateral Close <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<mat-icon matTooltip="Feerate for commitment transaction in a live channel which we originally funded" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> Penalty
</h4> <mat-icon matTooltip="Feerate to start at when penalizing a cheat attempt" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.unilateral_close | number}}</div> </h4>
</div> <div class="overflow-wrap dashboard-info-value">{{perkbw?.penalty | number}}</div>
<div> </div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> <div *ngIf="perkbw?.estimates && perkbw?.estimates.length && perkbw?.estimates.length > 3">
Delayed To Us <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<mat-icon matTooltip="Feerate for returning unilateral close funds to our wallet" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> 2 Blocks
</h4> <mat-icon matTooltip="Fee rate estimate for 2 blocks" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.delayed_to_us | number}}</div> </h4>
</div> <div class="overflow-wrap dashboard-info-value">{{perkbw?.estimates[0].smoothed_feerate | number}}</div>
</div> </div>
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="my-2"> <div *ngIf="perkbw?.estimates && perkbw?.estimates.length && perkbw?.estimates.length > 3">
<div> <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> 6 Blocks
Minimum Acceptable <mat-icon matTooltip="Fee rate estimate for 6 blocks" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<mat-icon matTooltip="The smallest feerate that you can use, usually the minimum relayed feerate of the backend" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> </h4>
</h4> <div class="overflow-wrap dashboard-info-value">{{perkbw?.estimates[1].smoothed_feerate | number}}</div>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.min_acceptable | number}}</div> </div>
</div> <div *ngIf="perkbw?.estimates && perkbw?.estimates.length && perkbw?.estimates.length > 3">
<div> <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> 12 Blocks
Maximum Acceptable <mat-icon matTooltip="Fee rate estimate for 12 blocks" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<mat-icon matTooltip="The largest feerate we will accept from remote negotiations. If a peer attempts to set the feerate higher than this we will unilaterally close the channel (or simply forget it if it's not open yet)" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> </h4>
</h4> <div class="overflow-wrap dashboard-info-value">{{perkbw?.estimates[2].smoothed_feerate | number}}</div>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.max_acceptable | number}}</div> </div>
</div> <div *ngIf="perkbw?.estimates && perkbw?.estimates.length && perkbw?.estimates.length > 3">
<div> <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> 100 Blocks
HTLC Resolution <mat-icon matTooltip="Fee rate estimate for 100 blocks" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<mat-icon matTooltip="Feerate for returning unilateral close HTLC outputs to our wallet" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> </h4>
</h4> <div class="overflow-wrap dashboard-info-value">{{perkbw?.estimates[3].smoothed_feerate | number}}</div>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.htlc_resolution | number}}</div> </div>
</div>
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Penalty
<mat-icon matTooltip="Feerate to start at when penalizing a cheat attempt" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.penalty | number}}</div>
</div> </div>
</div> </div>
</div> </div>

@ -1,3 +1,3 @@
.fee-rate-list .mat-list-item { .fee-rate-list .mat-list-item {
height: 44px; height: 44px;
} }

@ -13,14 +13,15 @@ export class CLNFeeRatesComponent implements AfterContentChecked {
@Input() feeRates: FeeRates; @Input() feeRates: FeeRates;
@Input() errorMessage: string; @Input() errorMessage: string;
perkbw: FeeRatePerObj = {}; perkbw: FeeRatePerObj = {};
displayedColumns: string[] = ['blockcount', 'feerate'];
constructor() { } constructor() { }
ngAfterContentChecked() { ngAfterContentChecked() {
if (this.feeRateStyle === feeRateStyle.KB) { if (this.feeRateStyle === feeRateStyle.KB) {
this.perkbw = this.feeRates.perkb!; this.perkbw = this.feeRates.perkb || {};
} else if (this.feeRateStyle === feeRateStyle.KW) { } else if (this.feeRateStyle === feeRateStyle.KW) {
this.perkbw = this.feeRates.perkw!; this.perkbw = this.feeRates.perkw || {};
} }
} }

@ -54,28 +54,28 @@ export class CLNNetworkInfoComponent implements OnInit, OnDestroy {
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 6, rows: 3 }, { id: 'node', icon: this.faServer, title: 'Node Information', cols: 6, rows: 3 },
{ id: 'status', icon: this.faNetworkWired, title: 'Channels', cols: 6, rows: 3 }, { id: 'status', icon: this.faNetworkWired, title: 'Channels', cols: 6, rows: 3 },
{ id: 'fee', icon: this.faBolt, title: 'Routing Fee', cols: 6, rows: 1 }, { id: 'fee', icon: this.faBolt, title: 'Routing Fee', cols: 6, rows: 1 },
{ id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 4, rows: 4 }, { id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 4, rows: 6 },
{ id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 4, rows: 4 }, { id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 4, rows: 6 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 4, rows: 4 } { id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 4, rows: 6 }
]; ];
this.nodeCardsOperator = [ this.nodeCardsOperator = [
{ id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 4, rows: 4 }, { id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 4, rows: 6 },
{ id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 4, rows: 4 }, { id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 4, rows: 6 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 4, rows: 4 } { id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 4, rows: 6 }
]; ];
} else { } else {
this.nodeCardsMerchant = [ this.nodeCardsMerchant = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 2, rows: 3 }, { id: 'node', icon: this.faServer, title: 'Node Information', cols: 2, rows: 3 },
{ id: 'status', icon: this.faNetworkWired, title: 'Channels', cols: 2, rows: 3 }, { id: 'status', icon: this.faNetworkWired, title: 'Channels', cols: 2, rows: 3 },
{ id: 'fee', icon: this.faBolt, title: 'Routing Fee', cols: 2, rows: 3 }, { id: 'fee', icon: this.faBolt, title: 'Routing Fee', cols: 2, rows: 3 },
{ id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 2, rows: 4 }, { id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 2, rows: 6 },
{ id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 2, rows: 4 }, { id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 2, rows: 6 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 2, rows: 4 } { id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 2, rows: 6 }
]; ];
this.nodeCardsOperator = [ this.nodeCardsOperator = [
{ id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 2, rows: 4 }, { id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 2, rows: 6 },
{ id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 2, rows: 4 }, { id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 2, rows: 6 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 2, rows: 4 } { id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 2, rows: 6 }
]; ];
} }
} }

@ -1,58 +1,60 @@
<div *ngIf="errorMessage?.trim() === ''; else errorBlock" fxLayout="column" fxLayout.gt-xs="row" fxFlex="100" fxLayoutAlign="stretch stretch"> <div fxLayout="column" fxFlex="100" fxLayoutAlign="stretch stretch">
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2"> <div *ngIf="errorMessage?.trim() === ''; else errorBlock" fxLayout="column" fxLayout.gt-xs="row" fxFlex="62" fxLayoutAlign="stretch stretch">
<div> <div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> <div>
Opening Channel <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<mat-icon matTooltip="Estimated cost of typical channel open" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> Opening Channel
</h4> <mat-icon matTooltip="Estimated cost of typical channel open" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.opening_channel_satoshis | number}}</div> </h4>
</div> <div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.opening_channel_satoshis | number}}</div>
<div> </div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> <div>
Mutual Close <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<mat-icon matTooltip="Estimated cost of typical channel close" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> Mutual Close
</h4> <mat-icon matTooltip="Estimated cost of typical channel close" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.mutual_close_satoshis | number}}</div> </h4>
</div> <div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.mutual_close_satoshis | number}}</div>
<div> </div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> <div>
Unilateral Close <h4 fxLayoutAlign="start start" class="dashboard-info-title">
<mat-icon matTooltip="Estimated cost of typical unilateral close (without HTLCs)" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon> Unilateral Close
</h4> <mat-icon matTooltip="Estimated cost of typical unilateral close (without HTLCs)" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.unilateral_close_satoshis | number}}</div> </h4>
</div> <div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.unilateral_close_satoshis | number}}</div>
<div fxFlex="12"> </div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title"></h4> <div fxFlex="12">
<div class="overflow-wrap dashboard-info-value"></div> <h4 fxLayoutAlign="start start" class="dashboard-info-title"></h4>
<div class="overflow-wrap dashboard-info-value"></div>
</div>
</div>
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2">
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
HTLC Timeout
<mat-icon matTooltip="Estimated cost of typical HTLC timeout transaction" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.htlc_timeout_satoshis | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
HTLC Success
<mat-icon matTooltip="Estimated cost of typical HTLC fulfillment transaction" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.htlc_success_satoshis | number}}</div>
</div>
<div fxFlex="12">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"></h4>
<div class="overflow-wrap dashboard-info-value"></div>
</div>
<div fxFlex="12">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"></h4>
<div class="overflow-wrap dashboard-info-value"></div>
</div>
</div> </div>
</div> </div>
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2"> <ng-template #errorBlock>
<div> <div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between" class="p-2">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"> <p>{{errorMessage}}</p>
HTLC Timeout
<mat-icon matTooltip="Estimated cost of typical HTLC timeout transaction" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.htlc_timeout_satoshis | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
HTLC Success
<mat-icon matTooltip="Estimated cost of typical HTLC fulfillment transaction" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.htlc_success_satoshis | number}}</div>
</div> </div>
<div fxFlex="12"> </ng-template>
<h4 fxLayoutAlign="start start" class="dashboard-info-title"></h4>
<div class="overflow-wrap dashboard-info-value"></div>
</div>
<div fxFlex="12">
<h4 fxLayoutAlign="start start" class="dashboard-info-title"></h4>
<div class="overflow-wrap dashboard-info-value"></div>
</div>
</div>
</div> </div>
<ng-template #errorBlock>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between" class="p-2">
<p>{{errorMessage}}</p>
</div>
</ng-template>

@ -51,7 +51,6 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="isCompatibleVersion" fxLayout="column" fxLayoutAlign="space-between stretch" fxLayoutAlign.gt-sm="space-between center" fxLayout.gt-sm="row wrap">
<mat-expansion-panel fxLayout="column" fxFlex="100" class="flat-expansion-panel mt-2" expanded="false" (closed)="onAdvancedPanelToggle(true)" (opened)="onAdvancedPanelToggle(false)"> <mat-expansion-panel fxLayout="column" fxFlex="100" class="flat-expansion-panel mt-2" expanded="false" (closed)="onAdvancedPanelToggle(true)" (opened)="onAdvancedPanelToggle(false)">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
@ -64,7 +63,7 @@
<mat-label>Coin Selection</mat-label> <mat-label>Coin Selection</mat-label>
<mat-select tabindex="8" multiple [(value)]="selUTXOs" (selectionChange)="onUTXOSelectionChange($event)"> <mat-select tabindex="8" multiple [(value)]="selUTXOs" (selectionChange)="onUTXOSelectionChange($event)">
<mat-select-trigger>{{totalSelectedUTXOAmount | number}} Sats ({{selUTXOs.length > 1 ? selUTXOs.length + ' UTXOs' : '1 UTXO'}})</mat-select-trigger> <mat-select-trigger>{{totalSelectedUTXOAmount | number}} Sats ({{selUTXOs.length > 1 ? selUTXOs.length + ' UTXOs' : '1 UTXO'}})</mat-select-trigger>
<mat-option *ngFor="let utxo of utxos" [value]="utxo">{{utxo.value | number}} Sats</mat-option> <mat-option *ngFor="let utxo of utxos" [value]="utxo">{{utxo.amount_msat/1000 | number:'1.0-0'}} Sats</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div fxFlex="60" fxLayout="row" fxLayoutAlign="start center"> <div fxFlex="60" fxLayout="row" fxLayoutAlign="start center">
@ -76,7 +75,6 @@
</div> </div>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
</div>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch"></div> <div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch"></div>
<div *ngIf="sendFundError !== ''" fxFlex="100" class="alert alert-danger mt-1"> <div *ngIf="sendFundError !== ''" fxFlex="100" class="alert alert-danger mt-1">
<fa-icon class="mr-1 alert-icon" [icon]="faExclamationTriangle"></fa-icon> <fa-icon class="mr-1 alert-icon" [icon]="faExclamationTriangle"></fa-icon>

@ -48,7 +48,6 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
public selectedAddress = ADDRESS_TYPES[1]; public selectedAddress = ADDRESS_TYPES[1];
public blockchainBalance: Balance = {}; public blockchainBalance: Balance = {};
public information: GetInfo = {}; public information: GetInfo = {};
public isCompatibleVersion = false;
public newAddress = ''; public newAddress = '';
public transaction: OnChain | any = {}; public transaction: OnChain | any = {};
public feeRateTypes = FEE_RATE_TYPES; public feeRateTypes = FEE_RATE_TYPES;
@ -140,9 +139,6 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
this.store.select(clnNodeInformation).pipe(takeUntil(this.unSubs[2])). this.store.select(clnNodeInformation).pipe(takeUntil(this.unSubs[2])).
subscribe((nodeInfo: GetInfo) => { subscribe((nodeInfo: GetInfo) => {
this.information = nodeInfo; this.information = nodeInfo;
this.isCompatibleVersion =
this.commonService.isVersionCompatible(this.information.version, '0.9.0') &&
this.commonService.isVersionCompatible(this.information.api_version, '0.4.0');
}); });
this.store.select(utxos).pipe(takeUntil(this.unSubs[3])). this.store.select(utxos).pipe(takeUntil(this.unSubs[3])).
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
@ -285,7 +281,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
onUTXOSelectionChange(event: any) { onUTXOSelectionChange(event: any) {
if (this.selUTXOs.length && this.selUTXOs.length > 0) { if (this.selUTXOs.length && this.selUTXOs.length > 0) {
this.totalSelectedUTXOAmount = this.selUTXOs?.reduce((total, curr) => (total + (curr.value || 0)), 0); this.totalSelectedUTXOAmount = this.selUTXOs?.reduce((total, curr) => (total + ((curr.amount_msat || 0) / 1000)), 0);
if (this.flgUseAllBalance) { if (this.flgUseAllBalance) {
this.onUTXOAllBalanceChange(); this.onUTXOAllBalanceChange();
} }

@ -31,7 +31,7 @@ export class CLNUTXOTablesComponent implements OnInit, OnDestroy {
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
if (utxosSeletor.utxos && utxosSeletor.utxos.length > 0) { if (utxosSeletor.utxos && utxosSeletor.utxos.length > 0) {
this.numUtxos = utxosSeletor.utxos.length || 0; this.numUtxos = utxosSeletor.utxos.length || 0;
this.numDustUtxos = utxosSeletor.utxos?.filter((utxo) => +(utxo.value || 0) < this.DUST_AMOUNT).length || 0; this.numDustUtxos = utxosSeletor.utxos?.filter((utxo) => (+(utxo.amount_msat || 0) / 1000) < this.DUST_AMOUNT).length || 0;
} }
this.logger.info(utxosSeletor); this.logger.info(utxosSeletor);
}); });

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

Loading…
Cancel
Save