Merge branch 'Release-0.14.0' into copy-fix

pull/1205/head
ShahanaFarooqui 11 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-call-expression": "off",
"@angular-eslint/template/no-inline-styles": "off",
"@angular-eslint/template/no-interpolation-in-attributes": "off",
"@angular-eslint/template/no-positive-tabindex": "off",
"@angular-eslint/template/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)
* [Start9Labs](https://start9labs.com)
* [Umbrel](https://github.com/getumbrel/umbrel)
* [Sovran Systems](https://sovransystems.com)
Docker Image: https://hub.docker.com/r/shahanafarooqui/rtl
@ -83,6 +84,7 @@ Example RTL-Config.json:
"multiPass": "password",
"port": "3000",
"defaultNodeIndex": 1,
"dbDirectoryPath": "<Complete path of the folder where rtl's database file should be saved>",
"SSO": {
"rtlSSO": 0,
"rtlCookiePath": "",

@ -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>",
"host": "<host for the rtl node server, default 'all IPs', Optional>",
"defaultNodeIndex": <Default index to load when rtl server starts, default 1, Optional>,
"dbDirectoryPath": "<Complete path of the folder where rtl database file should be saved, defults to RTL root, Optional>",
"SSO": {
"rtlSSO": <parameter to turn SSO off/on. Allowed values - 1 (single sign on via an external cookie), 0 (stand alone RTL authentication), 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)>",
"logoutRedirectLink": "<URL to re-direct to after logout/timeout from RTL, Required if SSO=1 else empty (Optional)>"
},
"nodes": [
{
"index": <Incremental node indices starting from 1, Required>,
"lnNode": "<Node name to uniquely identify the node in the UI, Default 'Node 1', Required>",
"lnImplementation": "<LNP implementation, Allowed values LND/CLN/ECL. Default 'LND', Required>",
"lnNode": "<Node name to uniquely identify the node in the UI, Required>",
"lnImplementation": "<LNP implementation, Allowed values LND/CLN/ECL, Required>",
"Authentication": {
"macaroonPath": "<Path for the folder containing 'admin.macaroon' (LND)/'access.macaroon' (CLN) file, Required for LND & CLN>",
"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>"
},
"Settings": {
"userPersona": "<User persona to tailor the data on UI. Allowed values MERCHANT, OPERATOR. Default MERCHANT, Required>",
"themeMode": "<Theme modes, Allowed values DAY, NIGHT. Default DAY, Required>",
"themeColor": "<Theme colors, Allowed values PURPLE, TEAL, INDIGO, PINK, YELLOW. Default PURPLE, 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, Optional>",
"themeColor": "<Theme colors, Allowed values PURPLE, TEAL, INDIGO, PINK, YELLOW. Default PURPLE, Optional>",
"channelBackupPath": "<Path to save channel backup file. Only for LND implementation, Default <RTL root>\backup\node-1, Optional>",
"bitcoindConfigPath": "<Path of bitcoind.conf path if available locally>",
"logLevel": <logging levels, will log in accordance with the logLevel value provided, Allowed values ERROR, WARN, INFO, DEBUG>,
"fiatConversion": <parameter to turn fiat conversion off/on. Allowed values - true, false, default false, Required>,
"currencyUnit": "<Optional: Fiat current Unit for currency conversion, default 'USD' If fiatConversion is true, Required if fiatConversion is true>",
"fiatConversion": <parameter to turn fiat conversion off/on. Allowed values - true, false, default false, Optional>,
"currencyUnit": "<Optional: Fiat current Unit for currency conversion, default 'USD', Optional>",
"unannouncedChannels": <parameter to turn off/on setting for opening announced Channels, default false, Optional>
"lnServerUrl": "<Service url for LND/Core Lightning REST APIs for the node, e.g. https://192.168.0.1:8080 OR https://192.168.0.1:3001 OR http://192.168.0.1:8080. Default 'https://127.0.0.1:8080', 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>",
"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 />
If the environment variables are set, it will take precedence over the parameters in the RTL-Config.json file.<br />
<br />
PORT (port number for the rtl node server, default 3000, 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 />
DB_DIRECTORY_PATH (Path for the folder where rtl database file should be saved, default RTL root directory, Optional)
APP_PASSWORD (Plaintext password to be provided by the parent container, NOT suggested for standalone RTL applications, to be used by Umbrel) (Optional)<br />
LN_IMPLEMENTATION (LND/CLN/ECL. Default 'LND', Required)<br />
LN_SERVER_URL (LN server URL for LNP REST APIs, default https://127.0.0.1:8080) (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) (Optional)<br />
SWAP_SERVER_URL (Swap server URL for REST APIs, default http://127.0.0.1:8081) (Optional)<br />
BOLTZ_SERVER_URL (Boltz server URL for REST APIs, default http://127.0.0.1:9003) (Optional)<br />
CONFIG_PATH (Full path of the LNP .conf file including the file name) (Optional for LND & CLN, Mandatory for ECL if LN_API_PASSWORD is undefined)<br />

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

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

@ -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
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
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.
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`).
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.
8. `swapMacaroonPath` should be set to the local path of the folder containing `loop.macaroon` file for loop.
9. `boltzMacaroonPath` should be set to the local path of the folder containing `admin.macaroon` file for boltz swaps.
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'
11. `swapServerUrl` must be set to the swap service url. e.g. https://127.0.0.1:8081.
12. `boltzServerUrl` must be set to the boltz service url. e.g. https://127.0.0.1:9003.
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.
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.
5. `dbDirectoryPath` should be set to the folder where RTL's database will be saved.
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. `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. `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. `swapMacaroonPath` should be set to the local path of the folder containing `loop.macaroon` file for loop.
10. `boltzMacaroonPath` should be set to the local path of the folder containing `admin.macaroon` file for boltz swaps.
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. `swapServerUrl` must be set to the swap service url. e.g. https://127.0.0.1:8081.
13. `boltzServerUrl` must be set to the boltz service url. e.g. https://127.0.0.1:9003.
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

@ -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'>",
"port": "3000",
"defaultNodeIndex": 1,
"dbDirectoryPath": "<Complete path of the folder where rtl's database file should be saved>",
"SSO": {
"rtlSSO": 0,
"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",
"port": "3000",
"defaultNodeIndex": 1,
"dbDirectoryPath": "C:\\Users\\xyz\\RTL",
"SSO": {
"rtlSSO": 0,
"rtlCookiePath": "",

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

@ -4,21 +4,48 @@ import { Common } from '../../utils/common.js';
let options = null;
const logger = Logger;
const common = Common;
export const listPeerChannels = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Peer Channels..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting 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/listChannels';
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.id.substring(0, 20);
}
const local = (channel.msatoshi_to_us) ? channel.msatoshi_to_us : 0;
const remote = (channel.msatoshi_to_them) ? channel.msatoshi_to_them : 0;
const total = channel.msatoshi_total ? channel.msatoshi_total : 0;
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;
});

@ -22,8 +22,8 @@ function paymentReducer(accumulator, currentPayment) {
}
function summaryReducer(accumulator, mpp) {
if (mpp.status === 'complete') {
accumulator.msatoshi = accumulator.msatoshi + mpp.msatoshi;
accumulator.msatoshi_sent = accumulator.msatoshi_sent + mpp.msatoshi_sent;
accumulator.amount_msat = accumulator.amount_msat + mpp.amount_msat;
accumulator.amount_sent_msat = accumulator.amount_sent_msat + mpp.amount_sent_msat;
accumulator.status = mpp.status;
}
if (mpp.bolt11) {
@ -47,10 +47,10 @@ function groupBy(payments) {
delete temp.partid;
}
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 = {
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
};
if (paySummary.bolt11) {
@ -104,8 +104,8 @@ export const postPayment = (req, res, next) => {
if (req.body.paymentType === 'OFFER') {
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() };
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;

@ -72,17 +72,12 @@ export class CLWebSocketClient {
this.wsServer.sendEventsToAllLNClients(msgStr, clWsClt.selectedNode);
};
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 });
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);
clWsClt.webSocketClient.close();
if (clWsClt.reConnect) {
this.reconnet(clWsClt);
}
}
else {
clWsClt.reConnect = false;
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 + ' }'));
this.wsServer.sendErrorToAllLNClients(errStr, clWsClt.selectedNode);
clWsClt.webSocketClient.close();
if (clWsClt.reConnect) {
this.reconnet(clWsClt);
}
};
};

@ -1,6 +1,9 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.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;
const logger = Logger;
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 });
});
};
// 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;
if (response.status && response.status.type === 'received') {
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;
}).catch((err) => {
@ -53,6 +53,20 @@ export const getInvoice = (req, res, next) => {
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting List Invoices..' });
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.ln_server_url + '/createinvoice';
options.form = req.body;
request.post(options).then((body) => {
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 });
});
createInvoiceRequestCall(req.session.selectedNode, req.body.description, req.body.amountMsat).then((invRes) => {
res.status(201).json(invRes);
}).catch((err) => 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 });
});
};
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([]);
}
};
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.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Websocket Server Heartbeat..' });
if (!eclWsClt.webSocketClient)
if (!eclWsClt.webSocketClient) {
return;
if (eclWsClt.webSocketClient.readyState !== 1)
}
if (eclWsClt.webSocketClient.readyState !== 1) {
return;
}
eclWsClt.webSocketClient.ping();
setTimeout(() => {
this.heartbeat(eclWsClt);

@ -176,6 +176,7 @@ export const postTransactions = (req, res, next) => {
options.form = JSON.stringify(options.form);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form });
request.post(options).then((body) => {
body = body.result ? body.result : body;
if (body.payment_error) {
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 });

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

@ -1,9 +1,10 @@
import exprs from 'express';
const { Router } = exprs;
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();
router.get('/listChannels', isAuthenticated, listChannels);
router.get('/listPeerChannels', isAuthenticated, listPeerChannels);
router.post('/', isAuthenticated, openChannel);
router.post('/setChannelFee', isAuthenticated, setChannelFee);
router.delete('/:channelId', isAuthenticated, closeChannel);

@ -1,11 +1,12 @@
import exprs from 'express';
const { Router } = exprs;
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();
router.get('/', isAuthenticated, getChannels);
router.get('/stats', isAuthenticated, getChannelStats);
router.post('/', isAuthenticated, openChannel);
router.post('/updateRelayFee', isAuthenticated, updateChannelRelayFee);
router.post('/circularRebalance', circularRebalance);
router.delete('/', isAuthenticated, closeChannel);
export default router;

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

@ -1,10 +1,11 @@
import exprs from 'express';
const { Router } = exprs;
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();
router.get('/route/', isAuthenticated, queryPaymentRoute);
router.get('/decode/:invoice', isAuthenticated, decodePayment);
router.post('/getsentinfos', isAuthenticated, getSentPaymentsInformation);
router.post('/sendtoroute', isAuthenticated, sendPaymentToRoute);
router.post('/', isAuthenticated, postPayment);
export default router;

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

@ -11,7 +11,8 @@ export class CommonService {
this.initSelectedNode = null;
this.rtl_conf_file_path = '';
this.port = 3000;
this.host = null;
this.host = '';
this.db_directory_path = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
this.rtl_pass = '';
this.flg_allow_password_update = true;
this.rtl_secret2fa = '';
@ -219,7 +220,11 @@ export class CommonService {
}
};
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) {
selectedNode = this.initSelectedNode;
}
@ -254,7 +259,7 @@ export class CommonService {
}
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: '' };
if (err.code && err.code === 'ENOENT') {
newErrorObj = {
@ -316,7 +321,7 @@ export class CommonService {
this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8');
}
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);
}
}
@ -328,7 +333,7 @@ export class CommonService {
this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8');
}
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);
}
}
@ -429,6 +434,7 @@ export class CommonService {
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: '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: 'DEFAULT NODE 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 configPath = '';
let channelBackupPath = '';
let dbPath = '';
switch (this.platform) {
case 'win32':
macaroonPath = homeDir + '\\AppData\\Local\\Lnd\\data\\chain\\bitcoin\\mainnet';
configPath = homeDir + '\\AppData\\Local\\Lnd\\lnd.conf';
channelBackupPath = homeDir + '\\backup\\node-1';
dbPath = homeDir + '\\database\\node-1';
break;
case 'darwin':
macaroonPath = homeDir + '/Library/Application Support/Lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/Library/Application Support/Lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1';
dbPath = homeDir + '/database/node-1';
break;
case 'linux':
macaroonPath = homeDir + '/.lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/.lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1';
dbPath = homeDir + '/database/node-1';
break;
default:
macaroonPath = '';
configPath = '';
channelBackupPath = '';
dbPath = '';
break;
}
const configData = {
port: '3000',
defaultNodeIndex: 1,
dbDirectoryPath: dbPath,
SSO: {
rtlSSO: 0,
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';
}
return configData;
@ -89,7 +95,7 @@ export class ConfigService {
};
this.updateLogByLevel = () => {
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 {
const RTLConfFile = this.common.rtl_conf_file_path + sep + 'RTL-Config.json';
const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8'));
@ -109,9 +115,9 @@ export class ConfigService {
}
};
this.validateNodeConfig = (config) => {
if ((+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() !== '') {
this.common.rtl_pass = this.hash.update(process.env.APP_PASSWORD).digest('hex');
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() !== '') {
this.common.rtl_pass = this.hash.update(process?.env?.APP_PASSWORD).digest('hex');
this.common.flg_allow_password_update = false;
}
else if (config.multiPassHashed && config.multiPassHashed !== '') {
@ -126,23 +132,24 @@ export class ConfigService {
this.common.rtl_secret2fa = config.secret2fa;
}
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.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.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.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) {
config.nodes.forEach((node, idx) => {
this.common.nodes[idx] = {};
this.common.nodes[idx].index = node.index;
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 !== 'ECL' && process.env.MACAROON_PATH && process.env.MACAROON_PATH.trim() !== '') {
this.common.nodes[idx].macaroon_path = process.env.MACAROON_PATH;
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;
}
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;
@ -151,8 +158,8 @@ export class ConfigService {
this.errMsg = 'Please set macaroon path for node index ' + node.index + ' in RTL-Config.json!';
}
if (this.common.nodes[idx].ln_implementation === 'ECL') {
if (process.env.LN_API_PASSWORD) {
this.common.nodes[idx].ln_api_password = process.env.LN_API_PASSWORD;
if (process?.env?.LN_API_PASSWORD) {
this.common.nodes[idx].ln_api_password = process?.env?.LN_API_PASSWORD;
}
else if (node.Authentication && 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 = '';
}
}
if (process.env.CONFIG_PATH) {
this.common.nodes[idx].config_path = process.env.CONFIG_PATH;
if (process?.env?.CONFIG_PATH) {
this.common.nodes[idx].config_path = process?.env?.CONFIG_PATH;
}
else if (node.Authentication && 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 !== '') {
try {
const exists = fs.existsSync(this.common.nodes[idx].config_path);
const exists = fs.existsSync(this.common.nodes[idx].config_path || '');
if (exists) {
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);
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 === '') {
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() !== '') {
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;
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;
}
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;
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;
}
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;
@ -218,9 +225,9 @@ export class ConfigService {
if (this.common.nodes[idx].fiat_conversion) {
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() !== '') {
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;
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_macaroon_path = process?.env?.SWAP_MACAROON_PATH;
}
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;
@ -230,9 +237,9 @@ export class ConfigService {
this.common.nodes[idx].swap_server_url = '';
this.common.nodes[idx].swap_macaroon_path = '';
}
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_macaroon_path = process.env.BOLTZ_MACAROON_PATH;
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_macaroon_path = process?.env?.BOLTZ_MACAROON_PATH;
}
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;
@ -242,10 +249,10 @@ export class ConfigService {
this.common.nodes[idx].boltz_server_url = '';
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_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].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].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].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;
try {
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');
@ -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.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;
if (fs.existsSync(log_file)) {
fs.writeFile(log_file, '', () => { });
if (fs.existsSync(log_file || '')) {
fs.writeFile((log_file || ''), '', () => { });
}
else {
try {
const directoryName = dirname(log_file);
const directoryName = dirname(log_file || '');
this.common.createDirectory(directoryName);
const createStream = fs.createWriteStream(log_file);
const createStream = fs.createWriteStream(log_file || '');
createStream.end();
}
catch (err) {
@ -292,14 +299,14 @@ export class ConfigService {
}
};
this.setSSOParams = (config) => {
if (process.env.RTL_SSO) {
this.common.rtl_sso = +process.env.RTL_SSO;
if (process?.env?.RTL_SSO) {
this.common.rtl_sso = +process?.env?.RTL_SSO;
}
else if (config.SSO && config.SSO.rtlSSO) {
this.common.rtl_sso = config.SSO.rtlSSO;
}
if (process.env.RTL_COOKIE_PATH) {
this.common.rtl_cookie_path = process.env.RTL_COOKIE_PATH;
if (process?.env?.RTL_COOKIE_PATH) {
this.common.rtl_cookie_path = process?.env?.RTL_COOKIE_PATH;
}
else if (config.SSO && config.SSO.rtlCookiePath) {
this.common.rtl_cookie_path = config.SSO.rtlCookiePath;
@ -307,8 +314,8 @@ export class ConfigService {
else {
this.common.rtl_cookie_path = '';
}
if (process.env.LOGOUT_REDIRECT_LINK) {
this.common.logout_redirect_link = process.env.LOGOUT_REDIRECT_LINK;
if (process?.env?.LOGOUT_REDIRECT_LINK) {
this.common.logout_redirect_link = process?.env?.LOGOUT_REDIRECT_LINK;
}
else if (config.SSO && config.SSO.logoutRedirectLink) {
this.common.logout_redirect_link = config.SSO.logoutRedirectLink;
@ -324,15 +331,15 @@ export class ConfigService {
};
this.setSelectedNode = (config) => {
if (config.defaultNodeIndex) {
this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex);
this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex) || {};
}
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 = () => {
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';
if (!fs.existsSync(confFileFullPath)) {
fs.writeFileSync(confFileFullPath, JSON.stringify(this.setDefaultConfig()));
@ -347,6 +354,7 @@ export class ConfigService {
throw new Error(err);
}
};
this.setServerConfiguration();
}
}
export const Config = new ConfigService();

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

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

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

@ -11,7 +11,7 @@ WORKDIR /RTL
COPY package.json /RTL/package.json
COPY package-lock.json /RTL/package-lock.json
RUN npm install
RUN npm install --legacy-peer-deps
# ---------------
# Build App
@ -25,7 +25,7 @@ RUN npm run buildfrontend
RUN npm run buildbackend
# Remove non production necessary modules
RUN npm prune --production
RUN npm prune --production --legacy-peer-deps
# ---------------
# 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
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
of this software and associated documentation files (the "Software"), to deal
@ -42,7 +42,7 @@ MIT
MIT
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
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
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".
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
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
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
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".
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
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
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
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".
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
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
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.**
@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
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>
<meta charset="utf-8">
<title>RTL</title>
<base href="/rtl/">
<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="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-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">
<meta i18n-content="" name="msapplication-TileColor" content="#da532c">
<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>
<body>
<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>window.global = window;</script>
<meta charset="utf-8">
<title>RTL</title>
<base href="/rtl/">
<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="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-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">
<meta i18n-content="" name="msapplication-TileColor" content="#da532c">
<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: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>
<rtl-app></rtl-app>
<script src="runtime.2136ac2986261ec4.js" type="module"></script><script src="polyfills.aa01d8f6b94657cb.js" type="module"></script><script src="main.3ccfe42677016a42.js" type="module"></script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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",
"version": "0.13.3-beta",
"version": "0.14.0-beta",
"license": "MIT",
"type": "module",
"scripts": {
@ -15,20 +15,12 @@
"server": "set 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",
"test": "ng test --watch=false",
"test": "ng test --watch=false --browsers=ChromeHeadless",
"lint": "ng lint",
"lintServer": "eslint ./server/**/* --ext .ts"
},
"private": true,
"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/store": "^15.0.0",
"@swimlane/ngx-charts": "^20.1.2",
@ -41,7 +33,7 @@
"express-session": "^1.17.3",
"hocon-parser": "^1.0.1",
"ini": "^3.0.1",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.0",
"ng-qrcode": "^8.0.1",
"ngx-perfect-scrollbar-next": "^10.1.1",
"otplib": "^12.0.1",
@ -62,11 +54,19 @@
"@angular-eslint/eslint-plugin-template": "^15.1.0",
"@angular-eslint/schematics": "^15.1.0",
"@angular-eslint/template-parser": "^15.1.0",
"@angular/animations": "^15.0.0",
"@angular/cdk": "^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/core": "^15.0.0",
"@angular/flex-layout": "^14.0.0-beta.41",
"@angular/forms": "^15.0.0",
"@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/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",

@ -1,8 +1,9 @@
import http from 'http';
import App from './backend/utils/app.js';
import { Logger } from './backend/utils/logger.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 App from './backend/utils/app.js';
const logger = Logger;
const common = Common;
@ -13,11 +14,11 @@ const onError = (error) => {
if (error.syscall !== 'listen') { throw error; }
switch (error.code) {
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);
break;
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);
break;
case 'ECONNREFUSED':
@ -35,7 +36,7 @@ const onError = (error) => {
};
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());
@ -45,7 +46,7 @@ server.on('listening', onListening);
wsServer.mount(server);
if (common.host) {
if (common.host && common.host !== '') {
server.listen(common.port, common.host);
} else {
server.listen(common.port);

@ -5,17 +5,41 @@ let options = null;
const logger: LoggerService = Logger;
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting 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/listChannels';
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.id.substring(0, 20); }
const local = (channel.msatoshi_to_us) ? channel.msatoshi_to_us : 0;
const remote = (channel.msatoshi_to_them) ? channel.msatoshi_to_them : 0;
const total = channel.msatoshi_total ? channel.msatoshi_total : 0;
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;
});

@ -22,8 +22,8 @@ function paymentReducer(accumulator, currentPayment) {
function summaryReducer(accumulator, mpp) {
if (mpp.status === 'complete') {
accumulator.msatoshi = accumulator.msatoshi + mpp.msatoshi;
accumulator.msatoshi_sent = accumulator.msatoshi_sent + mpp.msatoshi_sent;
accumulator.amount_msat = accumulator.amount_msat + mpp.amount_msat;
accumulator.amount_sent_msat = accumulator.amount_sent_msat + mpp.amount_sent_msat;
accumulator.status = mpp.status;
}
if (mpp.bolt11) { accumulator.bolt11 = mpp.bolt11; }
@ -43,10 +43,10 @@ function groupBy(payments) {
temp.total_parts = 1;
delete temp.partid;
} 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 = {
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
};
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.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() };
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; }
// eslint-disable-next-line arrow-body-style
return databaseService.validateDocument(CollectionsEnum.OFFERS, offerToUpdate).then((validated) => {

@ -52,7 +52,7 @@ export class CLWebSocketClient {
this.connectWithClient(clientExists);
}
}
} catch (err) {
} catch (err: any) {
throw new Error(err);
}
};
@ -85,15 +85,11 @@ export class CLWebSocketClient {
};
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 });
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);
clWsClt.webSocketClient.close();
if (clWsClt.reConnect) { this.reconnet(clWsClt); }
} else {
clWsClt.reConnect = false;
}
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 + ' }'));
this.wsServer.sendErrorToAllLNClients(errStr, clWsClt.selectedNode);
clWsClt.webSocketClient.close();
if (clWsClt.reConnect) { this.reconnet(clWsClt); }
};
};

@ -2,6 +2,10 @@ import request from 'request-promise';
import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.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;
const logger: LoggerService = Logger;
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 { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js';
import { CommonSelectedNode } from 'server/models/config.model.js';
let options = null;
const logger: LoggerService = Logger;
const common: CommonService = Common;
@ -18,7 +19,7 @@ export const getReceivedPaymentInfo = (lnServerUrl, invoice) => {
invoice.status = response.status.type;
if (response.status && response.status.type === 'received') {
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;
}).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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Getting List Invoices..' });
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) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Creating Invoice..' });
options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.ln_server_url + '/createinvoice';
options.form = req.body;
request.post(options).then((body) => {
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 });
});
createInvoiceRequestCall(req.session.selectedNode, req.body.description, req.body.amountMsat).then((invRes) => {
res.status(201).json(invRes);
}).catch((err) => res.status(err.statusCode).json({ message: err.message, error: err.error }));
};

@ -1,6 +1,7 @@
import request from 'request-promise';
import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js';
import { CommonSelectedNode } from 'server/models/config.model.js';
let options = null;
const logger: LoggerService = Logger;
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 });
});
};
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([]);
}
};
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);
}
}
} catch (err) {
} catch (err: any) {
throw new Error(err);
}
};
@ -97,7 +97,6 @@ export class ECLWebSocketClient {
eclWsClt.reConnect = false;
}
};
};
public disconnect = (selectedNode: CommonSelectedNode) => {
@ -121,13 +120,14 @@ export class ECLWebSocketClient {
public heartbeat = (eclWsClt) => {
this.logger.log({ selectedNode: eclWsClt.selectedNode, level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Websocket Server Heartbeat..' });
if (!eclWsClt.webSocketClient) return;
if (eclWsClt.webSocketClient.readyState !== 1) return;
if (!eclWsClt.webSocketClient) { return; }
if (eclWsClt.webSocketClient.readyState !== 1) { return; }
eclWsClt.webSocketClient.ping();
setTimeout(() => {
this.heartbeat(eclWsClt);
}, 59 * 1000);
}
};
}
export const ECLWSClient = new ECLWebSocketClient();

@ -166,6 +166,7 @@ export const postTransactions = (req, res, next) => {
options.form = JSON.stringify(options.form);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form });
request.post(options).then((body) => {
body = body.result ? body.result : body;
if (body.payment_error) {
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 });
@ -196,7 +197,7 @@ export const closeChannel = (req, res, next) => {
request.delete(options);
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.' });
} catch (error) {
} catch (error: any) {
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 });
}

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

@ -1,7 +1,7 @@
import { Database, DatabaseService } from '../../utils/database.js';
import { Logger, LoggerService } from '../../utils/logger.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 common: CommonService = Common;
@ -9,7 +9,7 @@ const databaseService: DatabaseService = Database;
export const getPageSettings = (req, res, next) => {
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 });
res.status(200).json(settings);
}).catch((errRes) => {

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

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

@ -1,11 +1,12 @@
import exprs from 'express';
const { Router } = exprs;
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();
router.get('/listChannels', isAuthenticated, listChannels);
router.get('/listPeerChannels', isAuthenticated, listPeerChannels);
router.post('/', isAuthenticated, openChannel);
router.post('/setChannelFee', isAuthenticated, setChannelFee);
router.delete('/:channelId', isAuthenticated, closeChannel);

@ -1,7 +1,7 @@
import exprs from 'express';
const { Router } = exprs;
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();
@ -9,6 +9,7 @@ router.get('/', isAuthenticated, getChannels);
router.get('/stats', isAuthenticated, getChannelStats);
router.post('/', isAuthenticated, openChannel);
router.post('/updateRelayFee', isAuthenticated, updateChannelRelayFee);
router.post('/circularRebalance', circularRebalance);
router.delete('/', isAuthenticated, closeChannel);
export default router;

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

@ -1,13 +1,14 @@
import exprs from 'express';
const { Router } = exprs;
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();
router.get('/route/', isAuthenticated, queryPaymentRoute);
router.get('/decode/:invoice', isAuthenticated, decodePayment);
router.post('/getsentinfos', isAuthenticated, getSentPaymentsInformation);
router.post('/sendtoroute', isAuthenticated, sendPaymentToRoute);
router.post('/', isAuthenticated, postPayment);
export default router;

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

@ -13,7 +13,8 @@ export class CommonService {
public initSelectedNode: CommonSelectedNode = null;
public rtl_conf_file_path = '';
public port = 3000;
public host = null;
public host = '';
public db_directory_path = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
public rtl_pass = '';
public flg_allow_password_update = true;
public rtl_secret2fa = '';
@ -236,7 +237,11 @@ export class CommonService {
};
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; }
switch (selectedNode.ln_implementation) {
case 'LND':
@ -270,7 +275,7 @@ export class CommonService {
if (err.options && err.options.headers) { delete err.options.headers; }
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: '' };
if (err.code && err.code === 'ENOENT') {
newErrorObj = {
@ -330,7 +335,7 @@ export class CommonService {
try {
this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8');
} 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);
}
} else {
@ -340,7 +345,7 @@ export class CommonService {
fs.writeFileSync(this.rtl_cookie_path, crypto.randomBytes(64).toString('hex'));
this.cookie_value = fs.readFileSync(this.rtl_cookie_path, 'utf-8');
} 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);
}
}
@ -441,6 +446,7 @@ export class CommonService {
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: '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: 'DEFAULT NODE 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 logger: LoggerService = Logger;
constructor() { }
constructor() {
this.setServerConfiguration();
}
private setDefaultConfig = () => {
const homeDir = os.userInfo().homedir;
let macaroonPath = '';
let configPath = '';
let channelBackupPath = '';
let dbPath = '';
switch (this.platform) {
case 'win32':
macaroonPath = homeDir + '\\AppData\\Local\\Lnd\\data\\chain\\bitcoin\\mainnet';
configPath = homeDir + '\\AppData\\Local\\Lnd\\lnd.conf';
channelBackupPath = homeDir + '\\backup\\node-1';
dbPath = homeDir + '\\database\\node-1';
break;
case 'darwin':
macaroonPath = homeDir + '/Library/Application Support/Lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/Library/Application Support/Lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1';
dbPath = homeDir + '/database/node-1';
break;
case 'linux':
macaroonPath = homeDir + '/.lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/.lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1';
dbPath = homeDir + '/database/node-1';
break;
default:
macaroonPath = '';
configPath = '';
channelBackupPath = '';
dbPath = '';
break;
}
const configData = {
port: '3000',
defaultNodeIndex: 1,
dbDirectoryPath: dbPath,
SSO: {
rtlSSO: 0,
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';
}
return configData;
@ -95,7 +103,7 @@ export class ConfigService {
private updateLogByLevel = () => {
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 {
const RTLConfFile = this.common.rtl_conf_file_path + sep + 'RTL-Config.json';
const config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8'));
@ -115,9 +123,9 @@ export class ConfigService {
};
private validateNodeConfig = (config) => {
if ((+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() !== '') {
this.common.rtl_pass = this.hash.update(process.env.APP_PASSWORD).digest('hex');
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() !== '') {
this.common.rtl_pass = this.hash.update(process?.env?.APP_PASSWORD).digest('hex');
this.common.flg_allow_password_update = false;
} else if (config.multiPassHashed && config.multiPassHashed !== '') {
this.common.rtl_pass = config.multiPassHashed;
@ -128,21 +136,22 @@ export class ConfigService {
}
this.common.rtl_secret2fa = config.secret2fa;
} 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.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.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.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) {
config.nodes.forEach((node, idx) => {
this.common.nodes[idx] = {};
this.common.nodes[idx].index = node.index;
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 !== 'ECL' && process.env.MACAROON_PATH && process.env.MACAROON_PATH.trim() !== '') {
this.common.nodes[idx].macaroon_path = process.env.MACAROON_PATH;
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;
} 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;
} 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 (process.env.LN_API_PASSWORD) {
this.common.nodes[idx].ln_api_password = process.env.LN_API_PASSWORD;
if (process?.env?.LN_API_PASSWORD) {
this.common.nodes[idx].ln_api_password = process?.env?.LN_API_PASSWORD;
} else if (node.Authentication && node.Authentication.lnApiPassword) {
this.common.nodes[idx].ln_api_password = node.Authentication.lnApiPassword;
} else {
this.common.nodes[idx].ln_api_password = '';
}
}
if (process.env.CONFIG_PATH) {
this.common.nodes[idx].config_path = process.env.CONFIG_PATH;
if (process?.env?.CONFIG_PATH) {
this.common.nodes[idx].config_path = process?.env?.CONFIG_PATH;
} else if (node.Authentication && node.Authentication.configPath) {
this.common.nodes[idx].config_path = node.Authentication.configPath;
} 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 !== '') {
try {
const exists = fs.existsSync(this.common.nodes[idx].config_path);
const exists = fs.existsSync(this.common.nodes[idx].config_path || '');
if (exists) {
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);
this.common.nodes[idx].ln_api_password = iniParsed['eclair.api.password'] ? iniParsed['eclair.api.password'] : parseHocon(configFile).eclair.api.password;
} 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!';
}
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;
} 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;
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;
} 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;
} 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;
} else if (node.Settings.lndServerUrl && node.Settings.lndServerUrl.trim() !== '') {
@ -207,9 +216,9 @@ export class ConfigService {
if (this.common.nodes[idx].fiat_conversion) {
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() !== '') {
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;
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_macaroon_path = process?.env?.SWAP_MACAROON_PATH;
} 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_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_macaroon_path = '';
}
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_macaroon_path = process.env.BOLTZ_MACAROON_PATH;
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_macaroon_path = process?.env?.BOLTZ_MACAROON_PATH;
} 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_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_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_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].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].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].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;
try {
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');
@ -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.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;
if (fs.existsSync(log_file)) {
fs.writeFile(log_file, '', () => { });
if (fs.existsSync(log_file || '')) {
fs.writeFile((log_file || ''), '', () => { });
} else {
try {
const directoryName = dirname(log_file);
const directoryName = dirname(log_file || '');
this.common.createDirectory(directoryName);
const createStream = fs.createWriteStream(log_file);
const createStream = fs.createWriteStream(log_file || '');
createStream.end();
} 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 });
@ -271,22 +280,22 @@ export class ConfigService {
};
private setSSOParams = (config) => {
if (process.env.RTL_SSO) {
this.common.rtl_sso = +process.env.RTL_SSO;
if (process?.env?.RTL_SSO) {
this.common.rtl_sso = +process?.env?.RTL_SSO;
} else if (config.SSO && config.SSO.rtlSSO) {
this.common.rtl_sso = config.SSO.rtlSSO;
}
if (process.env.RTL_COOKIE_PATH) {
this.common.rtl_cookie_path = process.env.RTL_COOKIE_PATH;
if (process?.env?.RTL_COOKIE_PATH) {
this.common.rtl_cookie_path = process?.env?.RTL_COOKIE_PATH;
} else if (config.SSO && config.SSO.rtlCookiePath) {
this.common.rtl_cookie_path = config.SSO.rtlCookiePath;
} else {
this.common.rtl_cookie_path = '';
}
if (process.env.LOGOUT_REDIRECT_LINK) {
this.common.logout_redirect_link = process.env.LOGOUT_REDIRECT_LINK;
if (process?.env?.LOGOUT_REDIRECT_LINK) {
this.common.logout_redirect_link = process?.env?.LOGOUT_REDIRECT_LINK;
} else if (config.SSO && config.SSO.logoutRedirectLink) {
this.common.logout_redirect_link = config.SSO.logoutRedirectLink;
}
@ -302,15 +311,15 @@ export class ConfigService {
private setSelectedNode = (config) => {
if (config.defaultNodeIndex) {
this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex);
this.common.initSelectedNode = this.common.findNode(config.defaultNodeIndex) || {};
} 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 = () => {
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';
if (!fs.existsSync(confFileFullPath)) {
fs.writeFileSync(confFileFullPath, JSON.stringify(this.setDefaultConfig()));
@ -319,7 +328,7 @@ export class ConfigService {
this.updateLogByLevel();
this.validateNodeConfig(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 });
throw new Error(err);
}

@ -1,6 +1,5 @@
import * as fs from 'fs';
import { join, dirname, sep } from 'path';
import { fileURLToPath } from 'url';
import { join, sep } from 'path';
import { Common, CommonService } from '../utils/common.js';
import { Logger, LoggerService } from '../utils/logger.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 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 } } = {};
constructor() { }

@ -30,6 +30,7 @@ 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 { HOUR_SECONDS } from './shared/services/consts-enums-functions';
let isDevEnvironemt = false;
if (isDevMode()) { isDevEnvironemt = true; }
@ -41,7 +42,7 @@ if (isDevMode()) { isDevEnvironemt = true; }
routing,
LayoutModule,
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(
{ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer },
{
@ -56,7 +57,7 @@ if (isDevMode()) { isDevEnvironemt = true; }
declarations: [AppComponent],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
AuthGuard, SessionService, DataService, WebSocketClientService, LoopService, CommonService, BoltzService
SessionService, DataService, WebSocketClientService, LoopService, CommonService, BoltzService
],
bootstrap: [AppComponent]
})

@ -26,35 +26,35 @@ type PathMatch = 'full' | 'prefix' | undefined;
export const routes: Routes = [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'login' },
{ 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: 'ecl', loadChildren: () => import('./eclair/ecl.module').then((childModule) => childModule.ECLModule), 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: '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: 'app', component: AppSettingsComponent, canActivate: [AuthGuard] },
{ path: 'auth', component: AuthSettingsComponent, canActivate: [AuthGuard] },
{ path: 'bconfig', component: BitcoinConfigComponent, canActivate: [AuthGuard] }
{ path: 'app', component: AppSettingsComponent, canActivate: [AuthGuard()] },
{ path: 'auth', component: AuthSettingsComponent, 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: 'nodesettings', component: NodeSettingsComponent, canActivate: [AuthGuard] },
{ path: 'pglayout', component: PageSettingsComponent, canActivate: [AuthGuard] },
{ path: 'nodesettings', component: NodeSettingsComponent, 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: 'loop', component: LoopServiceSettingsComponent, canActivate: [AuthGuard] },
{ path: 'boltz', component: BoltzServiceSettingsComponent, canActivate: [AuthGuard] }
{ path: 'loop', component: LoopServiceSettingsComponent, canActivate: [AuthGuard()] },
{ path: 'boltz', component: BoltzServiceSettingsComponent, canActivate: [AuthGuard()] }
]
},
{ path: 'experimental', component: ExperimentalSettingsComponent, canActivate: [AuthGuard] },
{ path: 'lnconfig', component: LNPConfigComponent, canActivate: [AuthGuard] }
{ path: 'experimental', component: ExperimentalSettingsComponent, 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: 'loop', pathMatch: <PathMatch>'full', redirectTo: 'loop/loopout' },
{ 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, { 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 { 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 { CLNChannelActiveHTLCsTableComponent } from './peers-channels/channels/channels-tables/channel-active-htlcs-table/channel-active-htlcs-table.component';
import { CLNUnlockedGuard } from '../shared/services/auth.guard';
@ -121,11 +122,10 @@ import { CLNUnlockedGuard } from '../shared/services/auth.guard';
CLNOffersTableComponent,
CLNOfferBookmarksTableComponent,
CLNLiquidityAdsListComponent,
CLNOpenLiquidityChannelComponent
],
providers: [
CLNUnlockedGuard
CLNOpenLiquidityChannelComponent,
CLNChannelActiveHTLCsTableComponent
],
providers: [],
bootstrap: [CLNRootComponent]
})
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 { CLNFailedTransactionsComponent } from './routing/failed-transactions/failed-transactions.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 { CLNRoutingReportComponent } from './reports/routing/routing-report.component';
@ -43,69 +44,70 @@ export const ClnRoutes: Routes = [
path: '', component: CLNRootComponent,
children: [
{ 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: 'receive/:selTab', component: CLNOnChainReceiveComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'send/:selTab', component: CLNOnChainSendComponent, data: { sweepAll: false }, canActivate: [CLNUnlockedGuard] },
{ path: 'sweep/:selTab', component: CLNOnChainSendComponent, data: { sweepAll: true }, canActivate: [CLNUnlockedGuard] }
{ path: 'receive/:selTab', component: CLNOnChainReceiveComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'send/:selTab', component: CLNOnChainSendComponent, data: { sweepAll: false }, 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: 'channels', component: CLNChannelsTablesComponent, canActivate: [CLNUnlockedGuard], children: [
path: 'channels', component: CLNChannelsTablesComponent, canActivate: [CLNUnlockedGuard()], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'open' },
{ path: 'open', component: CLNChannelOpenTableComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'pending', component: CLNChannelPendingTableComponent, canActivate: [CLNUnlockedGuard] }
{ path: 'open', component: CLNChannelOpenTableComponent, 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: 'payments', component: CLNLightningPaymentsComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'invoices', component: CLNLightningInvoicesTableComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'offers', component: CLNOffersTableComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'offrBookmarks', component: CLNOfferBookmarksTableComponent, canActivate: [CLNUnlockedGuard] }
{ path: 'payments', component: CLNLightningPaymentsComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'invoices', component: CLNLightningInvoicesTableComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'offers', component: CLNOffersTableComponent, 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: 'sign', component: CLNSignComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'verify', component: CLNVerifyComponent, canActivate: [CLNUnlockedGuard] }
{ path: 'sign', component: CLNSignComponent, 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: 'forwardinghistory', component: CLNForwardingHistoryComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'failedtransactions', component: CLNFailedTransactionsComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'localfail', component: CLNLocalFailedTransactionsComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'routingpeers', component: CLNRoutingPeersComponent, canActivate: [CLNUnlockedGuard] }
{ path: 'forwardinghistory', component: CLNForwardingHistoryComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'failedtransactions', component: CLNFailedTransactionsComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'localfail', component: CLNLocalFailedTransactionsComponent, 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: 'routingreport', component: CLNRoutingReportComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'transactions', component: CLNTransactionsReportComponent, canActivate: [CLNUnlockedGuard] }
{ path: 'routingreport', component: CLNRoutingReportComponent, 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: 'lookups', component: CLNLookupsComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'queryroutes', component: CLNQueryRoutesComponent, canActivate: [CLNUnlockedGuard] }
{ path: 'lookups', component: CLNLookupsComponent, canActivate: [CLNUnlockedGuard()] },
{ path: 'queryroutes', component: CLNQueryRoutesComponent, canActivate: [CLNUnlockedGuard()] }
]
},
{ path: 'rates', component: CLNNetworkInfoComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'rates', component: CLNNetworkInfoComponent, canActivate: [CLNUnlockedGuard()] },
{ path: '**', component: NotFoundComponent },
{ path: 'network', redirectTo: 'rates' },
{ path: 'wallet', redirectTo: 'home' },

@ -19,12 +19,12 @@
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<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>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Amount (mSats)</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.amount_msat}}</span>
<h4 class="font-bold-500">Amount (Sats)</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.amount_msat / 1000 | number:'1.0-0'}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
@ -32,6 +32,11 @@
<span class="foreground-secondary-text">{{lookupResult[0]?.base_fee_millisatoshi | number}}</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">
<h4 class="font-bold-500">Channel Flags</h4>
<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>
</div>
<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">
<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>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<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>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
@ -73,13 +68,13 @@
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Satoshis</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.satoshis | number}}</span>
<h4 class="font-bold-500">Source</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.source}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Source</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.source}}</span>
<h4 class="font-bold-500">Destination</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.destination}}</span>
</div>
</div>
<div fxLayout="column" fxFlex="49" fxLayoutAlign="start stretch" class="mt-1 bordered-box padding-gap-large">
@ -104,8 +99,8 @@
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Amount (mSats)</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.amount_msat}}</span>
<h4 class="font-bold-500">Amount (Sats)</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.amount_msat / 1000 | number:'1.0-0'}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
@ -113,6 +108,11 @@
<span class="foreground-secondary-text">{{lookupResult[1]?.base_fee_millisatoshi | number}}</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">
<h4 class="font-bold-500">Channel Flags</h4>
<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>
</div>
<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">
<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>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<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>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
@ -154,13 +144,13 @@
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Satoshis</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.satoshis | number}}</span>
<h4 class="font-bold-500">Source</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.source}}</span>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="column">
<h4 class="font-bold-500">Source</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.source}}</span>
<h4 class="font-bold-500">Destination</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.destination}}</span>
</div>
</div>
</div>

@ -34,38 +34,39 @@ export class CLNLookupsComponent implements OnInit, OnDestroy {
public faSearch = faSearch;
public screenSize = '';
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) {
this.screenSize = this.commonService.getScreenSize();
}
ngOnInit() {
this.actions.
pipe(
takeUntil(this.unSubs[0]),
filter((action) => (action.type === CLNActions.SET_LOOKUP_CLN || action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN))
).subscribe((resLookup: any) => {
if (resLookup.type === CLNActions.SET_LOOKUP_CLN) {
this.flgLoading[0] = true;
switch (this.selectedFieldId) {
case 0:
this.nodeLookupValue = typeof resLookup.payload[0] !== 'object' ? { nodeid: '' } : JSON.parse(JSON.stringify(resLookup.payload[0]));
break;
case 1:
this.channelLookupValue = typeof resLookup.payload[0] !== 'object' ? [] : JSON.parse(JSON.stringify(resLookup.payload));
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';
if (window.history.state && window.history.state.lookupType) {
this.selectedFieldId = +window.history.state.lookupType || 0;
this.lookupKey = window.history.state.lookupValue || '';
}
this.actions.pipe(takeUntil(this.unSubs[0]), filter((action) => (action.type === CLNActions.SET_LOOKUP_CLN || action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN))
).subscribe((resLookup: any) => {
if (resLookup.type === CLNActions.SET_LOOKUP_CLN) {
this.flgLoading[0] = true;
switch (this.selectedFieldId) {
case 0:
this.nodeLookupValue = typeof resLookup.payload[0] !== 'object' ? { nodeid: '' } : JSON.parse(JSON.stringify(resLookup.payload[0]));
break;
case 1:
this.channelLookupValue = typeof resLookup.payload[0] !== 'object' ? [] : JSON.parse(JSON.stringify(resLookup.payload));
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';
}
});
}
onLookup(): boolean | void {

@ -42,11 +42,15 @@
<th *matHeaderCellDef mat-header-cell>
<div class="bordered-box table-actions-select btn-action" fxLayoutAlign="center center">Actions</div>
</th>
<td *matCellDef="let address" mat-cell>
<span fxLayoutAlign="end 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>
</span>
</td>
<td *matCellDef="let address" mat-cell fxLayoutAlign="end center">
<div class="bordered-box table-actions-select" fxLayoutAlign="center center">
<mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger>
<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>
<tr *matHeaderRowDef="displayedColumns;" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns;" mat-row></tr>

@ -1,4 +1,10 @@
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 { SharedModule } from '../../../../shared/shared.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@ -14,7 +20,8 @@ describe('CLNNodeLookupComponent', () => {
declarations: [CLNNodeLookupComponent],
imports: [
SharedModule,
BrowserAnimationsModule
BrowserAnimationsModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer })
],
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 { MatSort } from '@angular/material/sort';
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 { 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({
selector: 'rtl-cln-node-lookup',
templateUrl: './node-lookup.component.html',
styleUrls: ['./node-lookup.component.scss']
})
export class CLNNodeLookupComponent implements OnInit {
export class CLNNodeLookupComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@Input() lookupResult: LookupNode;
public featureDescriptions: string[] = [];
public addresses: any = new MatTableDataSource([]);
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() {
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.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() !== '') {
this.lookupResult.features = this.lookupResult.features.substring(this.lookupResult.features.length - 40);
const featureHex = parseInt(this.lookupResult.features, 16);
NODE_FEATURES_CLN.forEach((feature) => {
if (!!(featureHex & ((1 << feature.range.min) | (1 << feature.range.max)))) {
this.featureDescriptions.push(feature.description + '\n');
if (featureHex & (1 << feature.range.min)) {
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) {
@ -42,4 +71,11 @@ export class CLNNodeLookupComponent implements OnInit {
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 matColumnDef="msatoshi">
<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 matColumnDef="actions">
<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: '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: 'msatoshi', value: selHop.msatoshi, title: 'mSatoshi', width: 50, 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: 50, type: DataTypeEnum.STRING },
{ key: 'delay', value: selHop.delay, title: 'Delay', width: 50, type: DataTypeEnum.NUMBER }]
[{ key: 'amount_msat', value: selHop.amount_msat, title: 'Amount (mSat)', width: 34, type: DataTypeEnum.NUMBER },
{ key: 'direction', value: selHop.direction, title: 'Direction', width: 33, type: DataTypeEnum.STRING },
{ key: 'delay', value: selHop.delay, title: 'Delay', width: 33, type: DataTypeEnum.NUMBER }]
];
this.store.dispatch(openAlert({
payload: {

@ -1,17 +1,17 @@
<div *ngIf="errorMessage?.trim() === ''; else errorBlock" class="mt-1" fxLayout="column" fxFlex="100"fxLayoutAlign="space-between stretch">
<div>
<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>
</div>
<div>
<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>
</div>
<div>
<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>
<ng-template #errorBlock>

@ -15,18 +15,18 @@
<div class="channels-capacity-scroll" [perfectScrollbar]>
<div *ngIf="activeChannels && activeChannels.length > 0; else noChannelBlock" fxLayout="column"fxFlex="100">
<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}}">
{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}}
<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.peer_id) | slice:0:24}}{{(channel.alias || channel.peer_id).length > 25 ? '...' : ''}}
</a>
<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">
<fa-icon class="color-primary mr-3px" matTooltip="Balance Score" [icon]="faBalanceScale"></fa-icon>
({{channel.balancedness || 0 | number}})
</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>
<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>

@ -8,15 +8,15 @@
<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 *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}}">
{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}}
<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.peer_id) | slice:0:24}}{{(channel.alias || channel.peer_id).length > 25 ? '...' : ''}}
</a>
<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 === '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 === '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.to_us_msat/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
</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 === '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 === '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.to_us_msat/1000 || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar>
</div>
</div>
</div>

@ -16,15 +16,15 @@
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch">
<div>
<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>
<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>
<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>

@ -155,11 +155,11 @@ export class CLNHomeComponent implements OnInit, OnDestroy {
this.totalOutboundLiquidity = 0;
this.activeChannels = channelsSeletor.activeChannels;
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.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.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.to_us_msat ? channel.to_us_msat > 0 : false)), 'to_us_msat'))) || [];
this.activeChannels.forEach((channel) => {
this.totalInboundLiquidity = this.totalInboundLiquidity + Math.ceil((channel.msatoshi_to_them || 0) / 1000);
this.totalOutboundLiquidity = this.totalOutboundLiquidity + Math.floor((channel.msatoshi_to_us || 0) / 1000);
this.totalInboundLiquidity = this.totalInboundLiquidity + Math.ceil((channel.to_them_msat || 0) / 1000);
this.totalOutboundLiquidity = this.totalOutboundLiquidity + Math.floor((channel.to_us_msat || 0) / 1000);
});
this.channelsStatus.active.channels = channelsSeletor.activeChannels.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') {
this.sortField = 'Capacity';
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 y = (b.msatoshi_to_them ? +b.msatoshi_to_them : 0) + (b.msatoshi_to_them ? +b.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.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));
});
} else {

@ -141,7 +141,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
getLabel(column: string) {
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() {
@ -230,8 +230,10 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
if (lqNode.features && lqNode.features.trim() !== '') {
const featureHex = parseInt(lqNode.features, 16);
NODE_FEATURES_CLN.forEach((feature) => {
if (!!(featureHex & ((1 << feature.range.min) | (1 << feature.range.max)))) {
featureDescriptions.push(feature.description);
if (featureHex & (1 << feature.range.min)) {
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 fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2">
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Opening
<mat-icon matTooltip="Default feerate for fundchannel and withdraw" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.opening | number}}</div>
<div *ngIf="errorMessage?.trim() === ''; else errorBlock" fxLayout="column" fxFlex="100" fxLayoutAlign="stretch stretch">
<div fxLayout="column" fxLayout.gt-xs="row" fxFlex="100" fxLayoutAlign="stretch stretch">
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2">
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Opening
<mat-icon matTooltip="Default feerate for fundchannel and withdraw" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</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>
<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>
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="my-2">
<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>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
HTLC Resolution
<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>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.htlc_resolution | number}}</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 fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2">
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
HTLC Resolution
<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>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.htlc_resolution | number}}</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 *ngIf="perkbw?.estimates && perkbw?.estimates.length && perkbw?.estimates.length > 3">
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
2 Blocks
<mat-icon matTooltip="Fee rate estimate for 2 blocks" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.estimates[0].smoothed_feerate | number}}</div>
</div>
<div *ngIf="perkbw?.estimates && perkbw?.estimates.length && perkbw?.estimates.length > 3">
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
6 Blocks
<mat-icon matTooltip="Fee rate estimate for 6 blocks" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.estimates[1].smoothed_feerate | number}}</div>
</div>
<div *ngIf="perkbw?.estimates && perkbw?.estimates.length && perkbw?.estimates.length > 3">
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
12 Blocks
<mat-icon matTooltip="Fee rate estimate for 12 blocks" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.estimates[2].smoothed_feerate | number}}</div>
</div>
<div *ngIf="perkbw?.estimates && perkbw?.estimates.length && perkbw?.estimates.length > 3">
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
100 Blocks
<mat-icon matTooltip="Fee rate estimate for 100 blocks" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{perkbw?.estimates[3].smoothed_feerate | number}}</div>
</div>
</div>
</div>
</div>

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

@ -13,14 +13,15 @@ export class CLNFeeRatesComponent implements AfterContentChecked {
@Input() feeRates: FeeRates;
@Input() errorMessage: string;
perkbw: FeeRatePerObj = {};
displayedColumns: string[] = ['blockcount', 'feerate'];
constructor() { }
ngAfterContentChecked() {
if (this.feeRateStyle === feeRateStyle.KB) {
this.perkbw = this.feeRates.perkb!;
this.perkbw = this.feeRates.perkb || {};
} 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: 'status', icon: this.faNetworkWired, title: 'Channels', cols: 6, rows: 3 },
{ 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: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 4, rows: 4 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', 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: 6 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 4, rows: 6 }
];
this.nodeCardsOperator = [
{ id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 4, rows: 4 },
{ id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 4, rows: 4 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', 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: 6 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 4, rows: 6 }
];
} else {
this.nodeCardsMerchant = [
{ id: 'node', icon: this.faServer, title: 'Node Information', 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: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 2, rows: 4 },
{ id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 2, rows: 4 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', 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: 6 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', cols: 2, rows: 6 }
];
this.nodeCardsOperator = [
{ id: 'feeRatesKB', icon: this.faServer, title: 'Fee Rate Per KB', cols: 2, rows: 4 },
{ id: 'feeRatesKW', icon: this.faNetworkWired, title: 'Fee Rate Per KW', cols: 2, rows: 4 },
{ id: 'onChainFeeEstimates', icon: this.faLink, title: 'Onchain Fee Estimates (Sats)', 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: 6 },
{ 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="50" fxLayoutAlign="space-between stretch" class="mt-2">
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Opening Channel
<mat-icon matTooltip="Estimated cost of typical channel open" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<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">
Mutual Close
<mat-icon matTooltip="Estimated cost of typical channel close" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<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">
Unilateral Close
<mat-icon matTooltip="Estimated cost of typical unilateral close (without HTLCs)" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.unilateral_close_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 fxLayout="column" fxFlex="100" fxLayoutAlign="stretch stretch">
<div *ngIf="errorMessage?.trim() === ''; else errorBlock" fxLayout="column" fxLayout.gt-xs="row" fxFlex="62" fxLayoutAlign="stretch stretch">
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch" class="mt-2">
<div>
<h4 fxLayoutAlign="start start" class="dashboard-info-title">
Opening Channel
<mat-icon matTooltip="Estimated cost of typical channel open" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<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">
Mutual Close
<mat-icon matTooltip="Estimated cost of typical channel close" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<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">
Unilateral Close
<mat-icon matTooltip="Estimated cost of typical unilateral close (without HTLCs)" matTooltipPosition="below" class="info-icon info-icon-primary">info_outline</mat-icon>
</h4>
<div class="overflow-wrap dashboard-info-value">{{feeRates?.onchain_fee_estimates?.unilateral_close_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>
<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 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>
<ng-template #errorBlock>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between" class="p-2">
<p>{{errorMessage}}</p>
</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>
</ng-template>
</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>
</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-header>
<mat-panel-title>
@ -64,7 +63,7 @@
<mat-label>Coin Selection</mat-label>
<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-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-form-field>
<div fxFlex="60" fxLayout="row" fxLayoutAlign="start center">
@ -76,7 +75,6 @@
</div>
</div>
</mat-expansion-panel>
</div>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch"></div>
<div *ngIf="sendFundError !== ''" fxFlex="100" class="alert alert-danger mt-1">
<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 blockchainBalance: Balance = {};
public information: GetInfo = {};
public isCompatibleVersion = false;
public newAddress = '';
public transaction: OnChain | any = {};
public feeRateTypes = FEE_RATE_TYPES;
@ -140,9 +139,6 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
this.store.select(clnNodeInformation).pipe(takeUntil(this.unSubs[2])).
subscribe((nodeInfo: GetInfo) => {
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])).
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
@ -285,7 +281,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
onUTXOSelectionChange(event: any) {
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) {
this.onUTXOAllBalanceChange();
}

@ -31,7 +31,7 @@ export class CLNUTXOTablesComponent implements OnInit, OnDestroy {
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
if (utxosSeletor.utxos && 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);
});

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

Loading…
Cancel
Save