Merge pull request #1248 from Ride-The-Lightning/Release-0.14.0

Release 0.14.0
pull/1312/head
ShahanaFarooqui 12 months ago committed by GitHub
commit 5cb6e10bd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -200,6 +200,7 @@
"@angular-eslint/template/no-autofocus": "off", "@angular-eslint/template/no-autofocus": "off",
"@angular-eslint/template/no-call-expression": "off", "@angular-eslint/template/no-call-expression": "off",
"@angular-eslint/template/no-inline-styles": "off", "@angular-eslint/template/no-inline-styles": "off",
"@angular-eslint/template/no-interpolation-in-attributes": "off",
"@angular-eslint/template/no-positive-tabindex": "off", "@angular-eslint/template/no-positive-tabindex": "off",
"@angular-eslint/template/use-track-by-function": "off" "@angular-eslint/template/use-track-by-function": "off"
} }

1
.github/README.md vendored

@ -35,6 +35,7 @@ RTL is available on the below platforms/services:
* [BCubium](https://bgeometrics.com) * [BCubium](https://bgeometrics.com)
* [Start9Labs](https://start9labs.com) * [Start9Labs](https://start9labs.com)
* [Umbrel](https://github.com/getumbrel/umbrel) * [Umbrel](https://github.com/getumbrel/umbrel)
* [Sovran Systems](https://sovransystems.com)
Docker Image: https://hub.docker.com/r/shahanafarooqui/rtl Docker Image: https://hub.docker.com/r/shahanafarooqui/rtl

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

@ -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

@ -123,5 +123,8 @@
} }
} }
} }
},
"cli": {
"analytics": false
} }
} }

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

@ -22,8 +22,8 @@ function paymentReducer(accumulator, currentPayment) {
} }
function summaryReducer(accumulator, mpp) { function summaryReducer(accumulator, mpp) {
if (mpp.status === 'complete') { if (mpp.status === 'complete') {
accumulator.msatoshi = accumulator.msatoshi + mpp.msatoshi; accumulator.amount_msat = accumulator.amount_msat + mpp.amount_msat;
accumulator.msatoshi_sent = accumulator.msatoshi_sent + mpp.msatoshi_sent; accumulator.amount_sent_msat = accumulator.amount_sent_msat + mpp.amount_sent_msat;
accumulator.status = mpp.status; accumulator.status = mpp.status;
} }
if (mpp.bolt11) { if (mpp.bolt11) {
@ -47,10 +47,10 @@ function groupBy(payments) {
delete temp.partid; delete temp.partid;
} }
else { else {
const paySummary = curr?.reduce(summaryReducer, { msatoshi: 0, msatoshi_sent: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' }); const paySummary = curr?.reduce(summaryReducer, { amount_msat: 0, amount_sent_msat: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' });
temp = { temp = {
is_group: true, is_expanded: false, total_parts: (curr.length ? curr.length : 0), status: paySummary.status, payment_hash: curr[0].payment_hash, is_group: true, is_expanded: false, total_parts: (curr.length ? curr.length : 0), status: paySummary.status, payment_hash: curr[0].payment_hash,
destination: curr[0].destination, msatoshi: paySummary.msatoshi, msatoshi_sent: paySummary.msatoshi_sent, created_at: curr[0].created_at, destination: curr[0].destination, amount_msat: paySummary.amount_msat, amount_sent_msat: paySummary.amount_sent_msat, created_at: curr[0].created_at,
mpps: curr mpps: curr
}; };
if (paySummary.bolt11) { if (paySummary.bolt11) {

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

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

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

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

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

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

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

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

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

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

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

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

@ -10,9 +10,9 @@
<link i18n-rel="" rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5"> <link i18n-rel="" rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5">
<meta i18n-content="" name="msapplication-TileColor" content="#da532c"> <meta i18n-content="" name="msapplication-TileColor" content="#da532c">
<meta i18n-content="" name="theme-color" content="#ffffff"> <meta i18n-content="" name="theme-color" content="#ffffff">
<style>html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:100%}@media only screen and (max-width: 56.25em){html{font-size:90%}}@media only screen and (max-width: 37.5em){html{font-size:80%}}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}@font-face{font-family:Roboto;src:url(Roboto-Thin.f7a95c9c5999532c.woff2) format("woff2"),url(Roboto-Thin.c13c157cb81e8ebb.woff) format("woff");font-weight:100;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.b0e084abf689f393.woff2) format("woff2"),url(Roboto-ThinItalic.1111028df6cea564.woff) format("woff");font-weight:100;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Light.0e01b6cd13b3857f.woff2) format("woff2"),url(Roboto-Light.603ca9a537b88428.woff) format("woff");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.232ef4b20215f720.woff2) format("woff2"),url(Roboto-LightItalic.1b5e142f787151c8.woff) format("woff");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Regular.475ba9e4e2d63456.woff2) format("woff2"),url(Roboto-Regular.bcefbfee882bc1cb.woff) format("woff");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.e3a9ebdaac06bbc4.woff2) format("woff2"),url(Roboto-RegularItalic.0668fae6af0cf8c2.woff) format("woff");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Medium.457532032ceb0168.woff2) format("woff2"),url(Roboto-Medium.6e1ae5f0b324a0aa.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.872f7060602d55d2.woff2) format("woff2"),url(Roboto-MediumItalic.e06fb533801cbb08.woff) format("woff");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Bold.447291a88c067396.woff2) format("woff2"),url(Roboto-Bold.fc482e6133cf5e26.woff) format("woff");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.1b15168ef6fa4e16.woff2) format("woff2"),url(Roboto-BoldItalic.e26ba339b06f09f7.woff) format("woff");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Black.2eaa390d458c877d.woff2) format("woff2"),url(Roboto-Black.b25f67ad8583da68.woff) format("woff");font-weight:900;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.7dc03ee444552bc5.woff2) format("woff2"),url(Roboto-BlackItalic.c8dc642467cb3099.woff) format("woff");font-weight:900;font-style:italic}</style><link rel="stylesheet" href="styles.69f6c7558eabf011.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.69f6c7558eabf011.css"></noscript></head> <style>html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:95%}@media only screen and (max-width: 56.25em){html{font-size:90%}}@media only screen and (max-width: 37.5em){html{font-size:80%}}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}@font-face{font-family:Roboto;src:url(Roboto-Thin.f7a95c9c5999532c.woff2) format("woff2"),url(Roboto-Thin.c13c157cb81e8ebb.woff) format("woff");font-weight:100;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.b0e084abf689f393.woff2) format("woff2"),url(Roboto-ThinItalic.1111028df6cea564.woff) format("woff");font-weight:100;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Light.0e01b6cd13b3857f.woff2) format("woff2"),url(Roboto-Light.603ca9a537b88428.woff) format("woff");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.232ef4b20215f720.woff2) format("woff2"),url(Roboto-LightItalic.1b5e142f787151c8.woff) format("woff");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Regular.475ba9e4e2d63456.woff2) format("woff2"),url(Roboto-Regular.bcefbfee882bc1cb.woff) format("woff");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.e3a9ebdaac06bbc4.woff2) format("woff2"),url(Roboto-RegularItalic.0668fae6af0cf8c2.woff) format("woff");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Medium.457532032ceb0168.woff2) format("woff2"),url(Roboto-Medium.6e1ae5f0b324a0aa.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.872f7060602d55d2.woff2) format("woff2"),url(Roboto-MediumItalic.e06fb533801cbb08.woff) format("woff");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Bold.447291a88c067396.woff2) format("woff2"),url(Roboto-Bold.fc482e6133cf5e26.woff) format("woff");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.1b15168ef6fa4e16.woff2) format("woff2"),url(Roboto-BoldItalic.e26ba339b06f09f7.woff) format("woff");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Black.2eaa390d458c877d.woff2) format("woff2"),url(Roboto-Black.b25f67ad8583da68.woff) format("woff");font-weight:900;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.7dc03ee444552bc5.woff2) format("woff2"),url(Roboto-BlackItalic.c8dc642467cb3099.woff) format("woff");font-weight:900;font-style:italic}</style><link rel="stylesheet" href="styles.a04c018645a5044a.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.a04c018645a5044a.css"></noscript></head>
<body> <body>
<rtl-app></rtl-app> <rtl-app></rtl-app>
<script src="runtime.4565d399c2c0ce89.js" type="module"></script><script src="polyfills.9720483e1820202a.js" type="module"></script><script src="main.828ba9b6ca158ec5.js" type="module"></script> <script src="runtime.2136ac2986261ec4.js" type="module"></script><script src="polyfills.aa01d8f6b94657cb.js" type="module"></script><script src="main.3ccfe42677016a42.js" type="module"></script>
</body></html> </body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@ -1 +0,0 @@
(()=>{"use strict";var e,v={},m={};function r(e){var f=m[e];if(void 0!==f)return f.exports;var t=m[e]={id:e,loaded:!1,exports:{}};return v[e].call(t.exports,t,t.exports,r),t.loaded=!0,t.exports}r.m=v,e=[],r.O=(f,t,i,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,o]=e[n],s=!0,d=0;d<t.length;d++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[d]))?t.splice(d--,1):(s=!1,o<a&&(a=o));if(s){e.splice(n--,1);var u=i();void 0!==u&&(f=u)}}return f}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,i,o]},r.d=(e,f)=>{for(var t in f)r.o(f,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:f[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((f,t)=>(r.f[t](e,f),f),[])),r.u=e=>e+"."+{258:"74bd2ac767e7a584",267:"8f996ec2b4b156e0",564:"34d502899be1574e",636:"0bd12e29e4623b7e"}[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

2363
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "rtl", "name": "rtl",
"version": "0.13.6-beta", "version": "0.14.0-beta",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "scripts": {

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

@ -22,8 +22,8 @@ function paymentReducer(accumulator, currentPayment) {
function summaryReducer(accumulator, mpp) { function summaryReducer(accumulator, mpp) {
if (mpp.status === 'complete') { if (mpp.status === 'complete') {
accumulator.msatoshi = accumulator.msatoshi + mpp.msatoshi; accumulator.amount_msat = accumulator.amount_msat + mpp.amount_msat;
accumulator.msatoshi_sent = accumulator.msatoshi_sent + mpp.msatoshi_sent; accumulator.amount_sent_msat = accumulator.amount_sent_msat + mpp.amount_sent_msat;
accumulator.status = mpp.status; accumulator.status = mpp.status;
} }
if (mpp.bolt11) { accumulator.bolt11 = mpp.bolt11; } if (mpp.bolt11) { accumulator.bolt11 = mpp.bolt11; }
@ -43,10 +43,10 @@ function groupBy(payments) {
temp.total_parts = 1; temp.total_parts = 1;
delete temp.partid; delete temp.partid;
} else { } else {
const paySummary = curr?.reduce(summaryReducer, { msatoshi: 0, msatoshi_sent: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' }); const paySummary = curr?.reduce(summaryReducer, { amount_msat: 0, amount_sent_msat: 0, status: (curr[0] && curr[0].status) ? curr[0].status : 'failed' });
temp = { temp = {
is_group: true, is_expanded: false, total_parts: (curr.length ? curr.length : 0), status: paySummary.status, payment_hash: curr[0].payment_hash, is_group: true, is_expanded: false, total_parts: (curr.length ? curr.length : 0), status: paySummary.status, payment_hash: curr[0].payment_hash,
destination: curr[0].destination, msatoshi: paySummary.msatoshi, msatoshi_sent: paySummary.msatoshi_sent, created_at: curr[0].created_at, destination: curr[0].destination, amount_msat: paySummary.amount_msat, amount_sent_msat: paySummary.amount_sent_msat, created_at: curr[0].created_at,
mpps: curr mpps: curr
}; };
if (paySummary.bolt11) { temp.bolt11 = paySummary.bolt11; } if (paySummary.bolt11) { temp.bolt11 = paySummary.bolt11; }

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

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

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

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

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

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

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

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

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

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

@ -42,7 +42,7 @@ if (isDevMode()) { isDevEnvironemt = true; }
routing, routing,
LayoutModule, LayoutModule,
HammerModule, HammerModule,
UserIdleModule.forRoot({ idle: (HOUR_SECONDS-10), timeout: 10, ping: 12000 }), UserIdleModule.forRoot({ idle: (HOUR_SECONDS - 10), timeout: 10, ping: 12000 }),
StoreModule.forRoot( StoreModule.forRoot(
{ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer }, { root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer },
{ {
@ -57,7 +57,7 @@ if (isDevMode()) { isDevEnvironemt = true; }
declarations: [AppComponent], declarations: [AppComponent],
providers: [ providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
AuthGuard, SessionService, DataService, WebSocketClientService, LoopService, CommonService, BoltzService SessionService, DataService, WebSocketClientService, LoopService, CommonService, BoltzService
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -21,7 +21,7 @@
<ng-container matColumnDef="is_dust"> <ng-container matColumnDef="is_dust">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before" matTooltip="Dust/Nondust"></th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before" matTooltip="Dust/Nondust"></th>
<td *matCellDef="let utxo" mat-cell> <td *matCellDef="let utxo" mat-cell>
<span *ngIf="numDustUTXOs > 0 && !isDustUTXO && utxo.value < dustAmount; else emptySpace" matTooltip="Risk of dust attack" matTooltipPosition="right"> <span *ngIf="numDustUTXOs > 0 && !isDustUTXO && (utxo?.amount_msat / 1000) < dustAmount; else emptySpace" matTooltip="Risk of dust attack" matTooltipPosition="right">
<mat-icon fxLayoutAlign="start center" color="warn" class="small-icon">warning</mat-icon> <mat-icon fxLayoutAlign="start center" color="warn" class="small-icon">warning</mat-icon>
</span> </span>
</td> </td>
@ -65,8 +65,8 @@
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Value (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Value (Sats)</th>
<td *matCellDef="let utxo" mat-cell> <td *matCellDef="let utxo" mat-cell>
<span *ngIf="utxo.value > 0 || utxo.value === 0" fxLayoutAlign="end center">{{utxo.value | number}}</span> <span *ngIf="utxo.amount_msat > 0 || utxo.amount_msat === 0" fxLayoutAlign="end center">{{utxo.amount_msat / 1000 | number:'1.0-0'}}</span>
<span *ngIf="utxo.value < 0" fxLayoutAlign="end center" class="red">({{utxo.value * -1 | number}})</span> <span *ngIf="utxo.amount_msat < 0" fxLayoutAlign="end center" class="red">({{utxo.amount_msat / 1000 * -1 | number:'1.0-0'}})</span>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="blockheight"> <ng-container matColumnDef="blockheight">

@ -1,5 +1,4 @@
import { Component, ViewChild, Input, AfterViewInit, OnDestroy, OnInit } from '@angular/core'; import { Component, ViewChild, Input, AfterViewInit, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -55,13 +54,11 @@ export class CLNOnChainUtxosComponent implements OnInit, AfterViewInit, OnDestro
public apiCallStatusEnum = APICallStatusEnum; public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private router: Router, private camelCaseWithReplace: CamelCaseWithReplacePipe) { constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.screenSize = this.commonService.getScreenSize(); this.screenSize = this.commonService.getScreenSize();
} }
ngOnInit() { ngOnInit() {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.tableSetting.tableId = this.isDustUTXO ? 'dust_utxos' : 'utxos'; this.tableSetting.tableId = this.isDustUTXO ? 'dust_utxos' : 'utxos';
this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])). this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])).
subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => {
@ -90,7 +87,7 @@ export class CLNOnChainUtxosComponent implements OnInit, AfterViewInit, OnDestro
this.errorMessage = !this.apiCallStatus.message ? '' : (typeof (this.apiCallStatus.message) === 'object') ? JSON.stringify(this.apiCallStatus.message) : this.apiCallStatus.message; this.errorMessage = !this.apiCallStatus.message ? '' : (typeof (this.apiCallStatus.message) === 'object') ? JSON.stringify(this.apiCallStatus.message) : this.apiCallStatus.message;
} }
if (utxosSelector.utxos && utxosSelector.utxos.length > 0) { if (utxosSelector.utxos && utxosSelector.utxos.length > 0) {
this.dustUtxos = utxosSelector.utxos?.filter((utxo) => +(utxo.value || 0) < this.dustAmount); this.dustUtxos = utxosSelector.utxos?.filter((utxo) => +(utxo.amount_msat || 0) / 1000 < this.dustAmount);
this.utxos = utxosSelector.utxos; this.utxos = utxosSelector.utxos;
if (this.isDustUTXO) { if (this.isDustUTXO) {
if (this.dustUtxos && this.dustUtxos.length > 0 && this.sort && this.paginator && this.displayedColumns.length > 0) { if (this.dustUtxos && this.dustUtxos.length > 0 && this.sort && this.paginator && this.displayedColumns.length > 0) {
@ -125,7 +122,7 @@ export class CLNOnChainUtxosComponent implements OnInit, AfterViewInit, OnDestro
const reorderedUTXO = [ const reorderedUTXO = [
[{ key: 'txid', value: selUtxo.txid, title: 'Transaction ID', width: 100 }], [{ key: 'txid', value: selUtxo.txid, title: 'Transaction ID', width: 100 }],
[{ key: 'output', value: selUtxo.output, title: 'Output', width: 50, type: DataTypeEnum.NUMBER }, [{ key: 'output', value: selUtxo.output, title: 'Output', width: 50, type: DataTypeEnum.NUMBER },
{ key: 'value', value: selUtxo.value, title: 'Value (Sats)', width: 50, type: DataTypeEnum.NUMBER }], { key: 'amount_msat', value: (selUtxo.amount_msat || 0) / 1000, title: 'Value (Sats)', width: 50, type: DataTypeEnum.NUMBER }],
[{ key: 'status', value: this.commonService.titleCase(selUtxo.status || ''), title: 'Status', width: 50, type: DataTypeEnum.STRING }, [{ key: 'status', value: this.commonService.titleCase(selUtxo.status || ''), title: 'Status', width: 50, type: DataTypeEnum.STRING },
{ key: 'blockheight', value: selUtxo.blockheight, title: 'Blockheight', width: 50, type: DataTypeEnum.NUMBER }], { key: 'blockheight', value: selUtxo.blockheight, title: 'Blockheight', width: 50, type: DataTypeEnum.NUMBER }],
[{ key: 'address', value: selUtxo.address, title: 'Address', width: 100 }] [{ key: 'address', value: selUtxo.address, title: 'Address', width: 100 }]
@ -147,7 +144,7 @@ export class CLNOnChainUtxosComponent implements OnInit, AfterViewInit, OnDestro
getLabel(column: string) { getLabel(column: string) {
const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column); const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column);
return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform(returnColumn.column, '_') : column === 'is_dust' ? 'Dust' : this.commonService.titleCase(column); return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform((returnColumn.column || ''), '_') : column === 'is_dust' ? 'Dust' : this.commonService.titleCase(column);
} }
setFilterPredicate() { setFilterPredicate() {
@ -159,13 +156,17 @@ export class CLNOnChainUtxosComponent implements OnInit, AfterViewInit, OnDestro
break; break;
case 'is_dust': case 'is_dust':
rowToFilter = (rowData?.value || 0) < this.dustAmount ? 'dust' : 'nondust'; rowToFilter = (rowData?.amount_msat || 0) / 1000 < this.dustAmount ? 'dust' : 'nondust';
break; break;
case 'status': case 'status':
rowToFilter = rowData?.status?.toLowerCase() || ''; rowToFilter = rowData?.status?.toLowerCase() || '';
break; break;
case 'value':
rowToFilter = ((rowData?.amount_msat || 0) / 1000).toString();
break;
default: default:
rowToFilter = typeof rowData[this.selFilterBy] === 'undefined' ? '' : typeof rowData[this.selFilterBy] === 'string' ? rowData[this.selFilterBy].toLowerCase() : typeof rowData[this.selFilterBy] === 'boolean' ? (rowData[this.selFilterBy] ? 'yes' : 'no') : rowData[this.selFilterBy].toString(); rowToFilter = typeof rowData[this.selFilterBy] === 'undefined' ? '' : typeof rowData[this.selFilterBy] === 'string' ? rowData[this.selFilterBy].toLowerCase() : typeof rowData[this.selFilterBy] === 'boolean' ? (rowData[this.selFilterBy] ? 'yes' : 'no') : rowData[this.selFilterBy].toString();
break; break;
@ -179,7 +180,8 @@ export class CLNOnChainUtxosComponent implements OnInit, AfterViewInit, OnDestro
this.listUTXOs.sort = this.sort; this.listUTXOs.sort = this.sort;
this.listUTXOs.sortingDataAccessor = (data: UTXO, sortHeaderId: string) => { this.listUTXOs.sortingDataAccessor = (data: UTXO, sortHeaderId: string) => {
switch (sortHeaderId) { switch (sortHeaderId) {
case 'is_dust': return +(data.value || 0) < this.dustAmount; case 'is_dust': return ((data.amount_msat || 0) / 1000) < this.dustAmount;
case 'value': return ((data.amount_msat || 0) / 1000);
default: return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null; default: return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null;
} }
}; };

@ -12,7 +12,9 @@
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="50"> <div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Short Channel ID</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Short Channel ID</h4>
<span class="foreground-secondary-text">{{channel.short_channel_id}}</span> <span class="foreground-secondary-text go-to-link" matTooltip="Go To Graph Lookup" (click)="onGoToLink()" >
{{channel.short_channel_id}}
</span>
</div> </div>
<div fxFlex="50"> <div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Peer Alias</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Peer Alias</h4>
@ -30,49 +32,63 @@
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="100"> <div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Peer Public Key</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Peer Public Key</h4>
<span class="foreground-secondary-text">{{channel.id}}</span> <span class="foreground-secondary-text">{{channel.peer_id}}</span>
</div> </div>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="25"> <div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">mSatoshi to Us</h4> <h4 fxLayoutAlign="start" class="font-bold-500">State</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.msatoshi_to_us | number}}</span> <span class="overflow-wrap foreground-secondary-text">{{channel?.state | camelcaseWithReplace:'_'}}</span>
</div>
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Spendable (mSats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.spendable_msatoshi | number}}</span>
</div> </div>
<div fxFlex="25"> <div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Total (mSats)</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Connected</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.msatoshi_total | number}}</span> <span class="overflow-wrap foreground-secondary-text">{{channel.peer_connected ? 'Yes' : 'No'}}</span>
</div> </div>
<div fxFlex="25"> <div fxFlex="34">
<h4 fxLayoutAlign="start" class="font-bold-500">State</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Private</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.state}}</span> <span class="overflow-wrap foreground-secondary-text">{{channel.private ? 'Yes' : 'No'}}</span>
</div> </div>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="25"> <div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Our Reserve (Sats)</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Remote Balance (Sats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.our_channel_reserve_satoshis | number}}</span> <span class="overflow-wrap foreground-secondary-text">{{channel.to_them_msat / 1000 | number:'1.0-0'}}</span>
</div>
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Their Reserve (Sats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.their_channel_reserve_satoshis | number}}</span>
</div> </div>
<div fxFlex="25"> <div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Connected</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Local Balance (Sats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.connected ? 'Yes' : 'No'}}</span> <span class="overflow-wrap foreground-secondary-text">{{channel.to_us_msat / 1000 | number:'1.0-0'}}</span>
</div> </div>
<div fxFlex="25"> <div fxFlex="34">
<h4 fxLayoutAlign="start" class="font-bold-500">Private</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Total (Sats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.private ? 'Yes' : 'No'}}</span> <span class="overflow-wrap foreground-secondary-text">{{channel.total_msat / 1000 | number:'1.0-0'}}</span>
</div> </div>
</div> </div>
<mat-divider class="my-1"></mat-divider> <mat-divider class="my-1"></mat-divider>
<div *ngIf="showAdvanced"> <div *ngIf="showAdvanced">
<div fxLayout="row">
<div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Receivable (Sats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.receivable_msat / 1000 | number:'1.0-0'}}</span>
</div>
<div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Spendable (Sats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.spendable_msat / 1000 | number:'1.0-0'}}</span>
</div>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="row">
<div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Their Reserve (Sats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.their_reserve_msat / 1000 | number:'1.0-2'}}</span>
</div>
<div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Our Reserve (Sats)</h4>
<span class="overflow-wrap foreground-secondary-text">{{channel.our_reserve_msat / 1000 | number:'1.0-2'}}</span>
</div>
</div>
<mat-divider class="my-1"></mat-divider>
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="100"> <div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Funding Transaction ID</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Funding Transaction ID</h4>

@ -1,4 +1,5 @@
import { Component, OnInit, Inject } from '@angular/core'; import { Component, OnInit, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { faReceipt } from '@fortawesome/free-solid-svg-icons'; import { faReceipt } from '@fortawesome/free-solid-svg-icons';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
@ -24,7 +25,7 @@ export class CLNChannelInformationComponent implements OnInit {
public screenSize = ''; public screenSize = '';
public screenSizeEnum = ScreenSizeEnum; public screenSizeEnum = ScreenSizeEnum;
constructor(public dialogRef: MatDialogRef<CLNChannelInformationComponent>, @Inject(MAT_DIALOG_DATA) public data: CLNChannelInformation, private logger: LoggerService, private commonService: CommonService, private snackBar: MatSnackBar) { } constructor(public dialogRef: MatDialogRef<CLNChannelInformationComponent>, @Inject(MAT_DIALOG_DATA) public data: CLNChannelInformation, private logger: LoggerService, private commonService: CommonService, private snackBar: MatSnackBar, private router: Router) { }
ngOnInit() { ngOnInit() {
this.channel = this.data.channel; this.channel = this.data.channel;
@ -45,4 +46,9 @@ export class CLNChannelInformationComponent implements OnInit {
this.logger.info('Copied Text: ' + payload); this.logger.info('Copied Text: ' + payload);
} }
onGoToLink() {
this.router.navigateByUrl('/cln/graph/lookups', { state: { lookupType: '1', lookupValue: this.channel.short_channel_id } }); // 1 = Channel
this.onClose();
}
} }

@ -0,0 +1,146 @@
<div fxLayout="column" class="padding-gap">
<div fxLayout="column" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70"></div>
<div fxFlex.gt-xs="30" fxLayoutAlign.gt-xs="space-between center" fxLayout="row" fxLayoutAlign="space-between stretch">
<mat-form-field fxLayout="column" fxFlex="49">
<mat-label>Filter By</mat-label>
<mat-select tabindex="1" name="filterBy" [(ngModel)]="selFilterBy" (selectionChange)="selFilter=''; applyFilter()">
<perfect-scrollbar><mat-option *ngFor="let column of ['all'].concat(displayedColumns.slice(0, -1))" [value]="column">{{getLabel(column)}}</mat-option></perfect-scrollbar>
</mat-select>
</mat-form-field>
<mat-form-field fxLayout="column" fxFlex="49">
<mat-label>Filter</mat-label>
<input matInput name="filter" [(ngModel)]="selFilter" (input)="applyFilter()" (keyup)="applyFilter()">
</mat-form-field>
</div>
</div>
<div fxLayout="column" fxFlex="100" class="table-container" [perfectScrollbar]>
<mat-progress-bar *ngIf="apiCallStatus.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table #table mat-table fxFlex="100" matSort [matSortActive]="tableSetting.sortBy" [matSortDirection]="tableSetting.sortOrder" [dataSource]="channels" [ngClass]="{'error-border': errorMessage !== ''}">
<!-- Channel Group Row Start -->
<ng-container matColumnDef="amount_msat">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Amount (Sats)</th>
<td *matCellDef="let channel" mat-cell>
<span fxLayoutAlign="start center" class="htlc-row-span">
Active HTLCs: {{channel?.htlcs?.length}}
</span>
<ng-container *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs; index as i" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.amount_msat / 1000 | number:'1.0-2'}}
</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="direction">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Alias/Direction</th>
<td *matCellDef="let channel" mat-cell>
<span fxLayoutAlign="start center" class="htlc-row-span">{{channel?.alias}}</span>
<ng-container *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="start center" class="htlc-row-span">
{{htlc?.direction | titlecase}}
</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="id">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">
<span fxLayoutAlign="end center" class="htlc-row-span">HTLC ID</span>
</th>
<td *matCellDef="let channel" mat-cell>
<span fxLayoutAlign="end center" class="htlc-row-span">{{channel?.id}}</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.id | number}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="expiry">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">
<span fxLayoutAlign="end center" class="htlc-row-span">Expiry</span>
</th>
<td *matCellDef="let channel" mat-cell>
<span fxLayoutAlign="end center" class="htlc-row-span">{{' '}}</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.expiry | number:'1.0-0'}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before" class="pl-3 htlc-row-span">
<span fxLayoutAlign="end center" class="htlc-row-span">State</span>
</th>
<td *matCellDef="let channel" mat-cell class="pl-3">
<span fxLayoutAlign="end center" class="htlc-row-span">{{' '}}</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.state | camelcaseWithReplace:'_'}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="local_trimmed">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before" class="pl-3 htlc-row-span">
<span fxLayoutAlign="end center" class="htlc-row-span">Local Trimmed</span>
</th>
<td *matCellDef="let channel" mat-cell class="pl-3">
<span fxLayoutAlign="end center" class="htlc-row-span">{{' '}}</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.local_trimmed ? 'Yes' : 'No'}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="payment_hash">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before" class="pl-3 htlc-row-span">
<span fxLayoutAlign="end center" class="htlc-row-span">Payment Hash</span>
</th>
<td *matCellDef="let channel" mat-cell class="pl-3">
<span fxLayout="row" class="ellipsis-parent htlc-row-span" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '6rem' : colWidth}">
<span fxLayoutAlign="end center" class="ellipsis-child">{{' '}}</span>
</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="start center" class="ellipsis-parent htlc-row-span" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '6rem' : colWidth}">
<span class="ellipsis-child">{{htlc?.payment_hash}}</span>
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef mat-header-cell class="px-2">
<div class="bordered-box table-actions-select" fxLayoutAlign="end center">
<mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger>
<mat-option (click)="onDownloadCSV()">Download CSV</mat-option>
</mat-select>
</div>
</th>
<td *matCellDef="let channel" mat-cell class="px-2" fxLayout="column" fxLayoutAlign="center end">
<span fxLayoutAlign="end center" class="htlc-group-head">
<button mat-flat-button class="btn-htlc-expand" color="primary" type="button" tabindex="5" (click)="channel.is_expanded = !channel.is_expanded">{{channel.is_expanded ? 'Hide' : 'Show'}}</button>
</span>
<div *ngIf="channel.is_expanded">
<div *ngFor="let htlc of channel?.htlcs; index as i" class="htlc-group-details" fxLayoutAlign="end center">
<button mat-stroked-button class="btn-htlc-info" color="primary" type="button" tabindex="6" (click)="onHTLCClick(htlc, channel)">View {{i + 1}}</button>
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="no_channel">
<td *matFooterCellDef mat-footer-cell colspan="4">
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.COMPLETED">No active htlc available.</p>
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.INITIATED">Getting active htlcs...</p>
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.ERROR">{{errorMessage}}</p>
</td>
</ng-container>
<!-- channel Group Row End -->
<tr *matFooterRowDef="['no_channel']" mat-footer-row [ngClass]="{'display-none': channels?.data && channels?.data?.length>0}"></tr>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns;" mat-row></tr>
</table>
</div>
<mat-paginator class="mb-1" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" [showFirstLastButtons]="screenSize === screenSizeEnum.XS ? false : true"></mat-paginator>
</div>

@ -0,0 +1,34 @@
@import "../../../../../shared/theme/styles/constants.scss";
.mat-column-amount_msat {
.htlc-row-span:not(:first-of-type) {
padding-left: 2rem;
padding-right: 2rem;
}
}
.htlc-row-span {
min-height: 3rem;
&.ellipsis-parent {
display: flex;
align-items: center;
}
}
.mat-column-actions {
& .htlc-group-head, & .htlc-group-details {
min-height: 3rem;
}
& .btn-htlc-expand {
min-width: $table-actions-min-width;
width: $table-actions-min-width;
margin: 0;
}
& .btn-htlc-info {
min-width: $table-actions-min-width - 1rem;
min-width: $table-actions-min-width - 1rem;
margin: 0;
}
}

@ -0,0 +1,51 @@
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 { CommonService } from '../../../../../shared/services/common.service';
import { LoggerService } from '../../../../../shared/services/logger.service';
import { CLNChannelActiveHTLCsTableComponent } from './channel-active-htlcs-table.component';
import { mockDataService, mockLoggerService } from '../../../../../shared/test-helpers/mock-services';
import { SharedModule } from '../../../../../shared/shared.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DataService } from '../../../../../shared/services/data.service';
describe('CLNChannelActiveHTLCsTableComponent', () => {
let component: CLNChannelActiveHTLCsTableComponent;
let fixture: ComponentFixture<CLNChannelActiveHTLCsTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [CLNChannelActiveHTLCsTableComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer })
],
providers: [
CommonService,
{ provide: LoggerService, useClass: mockLoggerService },
{ provide: DataService, useClass: mockDataService }
]
}).
compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLNChannelActiveHTLCsTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
afterEach(() => {
TestBed.resetTestingModule();
});
});

@ -0,0 +1,242 @@
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Channel, ChannelHTLC } from '../../../../../shared/models/clnModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, APICallStatusEnum, SortOrderEnum, CLN_DEFAULT_PAGE_SETTINGS, CLN_PAGE_DEFS } from '../../../../../shared/services/consts-enums-functions';
import { ApiCallStatusPayload } from '../../../../../shared/models/apiCallsPayload';
import { LoggerService } from '../../../../../shared/services/logger.service';
import { CommonService } from '../../../../../shared/services/common.service';
import { openAlert } from '../../../../../store/rtl.actions';
import { RTLState } from '../../../../../store/rtl.state';
import { clnPageSettings, channels } from '../../../../store/cln.selector';
import { ColumnDefinition, PageSettings, TableSetting } from '../../../../../shared/models/pageSettings';
import { CamelCaseWithReplacePipe } from '../../../../../shared/pipes/app.pipe';
import { MAT_SELECT_CONFIG } from '@angular/material/select';
@Component({
selector: 'rtl-cln-channel-active-htlcs-table',
templateUrl: './channel-active-htlcs-table.component.html',
styleUrls: ['./channel-active-htlcs-table.component.scss'],
providers: [
{ provide: MAT_SELECT_CONFIG, useValue: { overlayPanelClass: 'rtl-select-overlay' } },
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('HTLCs') }
]
})
export class CLNChannelActiveHTLCsTableComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | undefined;
public nodePageDefs = CLN_PAGE_DEFS;
public selFilterBy = 'all';
public colWidth = '20rem';
public PAGE_ID = 'peers_channels';
public tableSetting: TableSetting = { tableId: 'active_HTLCs', recordsPerPage: PAGE_SIZE, sortBy: 'expiry', sortOrder: SortOrderEnum.DESCENDING };
public channels: any = new MatTableDataSource([]);
public channelsJSONArr: Channel[] = [];
public displayedColumns: any[] = [];
public htlcColumns = [];
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
public errorMessage = '';
public selFilter = '';
public apiCallStatus: ApiCallStatusPayload | null = null;
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.screenSize = this.commonService.getScreenSize();
}
ngOnInit() {
this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])).
subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = settings.apiCallStatus;
if (this.apiCallStatus.status === APICallStatusEnum.ERROR) {
this.errorMessage = this.apiCallStatus.message || '';
}
this.tableSetting = settings.pageSettings.find((page) => page.pageId === this.PAGE_ID)?.tables.find((table) => table.tableId === this.tableSetting.tableId) || CLN_DEFAULT_PAGE_SETTINGS.find((page) => page.pageId === this.PAGE_ID)?.tables.find((table) => table.tableId === this.tableSetting.tableId)!;
if (this.screenSize === ScreenSizeEnum.XS || this.screenSize === ScreenSizeEnum.SM) {
this.displayedColumns = JSON.parse(JSON.stringify(this.tableSetting.columnSelectionSM));
} else {
this.displayedColumns = JSON.parse(JSON.stringify(this.tableSetting.columnSelection));
}
this.displayedColumns.push('actions');
this.pageSize = this.tableSetting.recordsPerPage ? +this.tableSetting.recordsPerPage : PAGE_SIZE;
this.colWidth = this.displayedColumns.length ? ((this.commonService.getContainerSize().width / this.displayedColumns.length) / 14) + 'rem' : '20rem';
this.logger.info(this.displayedColumns);
});
this.store.select(channels).pipe(takeUntil(this.unSubs[1])).
subscribe((channelsSelector: { activeChannels: Channel[], pendingChannels: Channel[], inactiveChannels: Channel[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = channelsSelector.apiCallStatus;
if (this.apiCallStatus.status === APICallStatusEnum.ERROR) {
this.errorMessage = !this.apiCallStatus.message ? '' : (typeof (this.apiCallStatus.message) === 'object') ? JSON.stringify(this.apiCallStatus.message) : this.apiCallStatus.message;
}
const allChannels = [...channelsSelector.activeChannels, ...channelsSelector.pendingChannels, ...channelsSelector.inactiveChannels];
this.channelsJSONArr = allChannels?.filter((channel) => channel.htlcs && channel.htlcs.length > 0) || [];
if (this.channelsJSONArr.length > 0 && this.sort && this.paginator && this.displayedColumns.length > 0) {
this.loadHTLCsTable(this.channelsJSONArr);
}
this.logger.info(channelsSelector);
});
}
ngAfterViewInit() {
if (this.channelsJSONArr.length > 0) {
this.loadHTLCsTable(this.channelsJSONArr);
}
}
onHTLCClick(selHtlc: ChannelHTLC, selChannel: Channel) {
const reorderedHTLC = [
[{ key: 'alias', value: selChannel.alias, title: 'Alias', width: 100, type: DataTypeEnum.STRING }],
[{ key: 'amount_msat', value: ((selHtlc.amount_msat || 0) / 1000), title: 'Amount (Sats)', width: 50, type: DataTypeEnum.NUMBER },
{ key: 'direction', value: this.commonService.titleCase(selHtlc.direction || ''), title: 'Direction', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'expiry', value: selHtlc.expiry, title: 'Expiry', width: 50, type: DataTypeEnum.NUMBER },
{ key: 'state', value: this.camelCaseWithReplace.transform((selHtlc.state || '') || '', '_'), title: 'State', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'id', value: selHtlc.id, title: 'HTLC ID', width: 50, type: DataTypeEnum.STRING },
{ key: 'local_trimmed', value: selHtlc.local_trimmed, title: 'Local Trimmed', width: 50, type: DataTypeEnum.BOOLEAN }],
[{ key: 'payment_hash', value: selHtlc.payment_hash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING }]
];
this.store.dispatch(openAlert({
payload: {
data: {
type: AlertTypeEnum.INFORMATION,
alertTitle: 'HTLC Information',
message: reorderedHTLC
}
}
}));
}
applyFilter() {
this.channels.filter = this.selFilter.trim().toLowerCase();
}
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);
}
setFilterPredicate() {
this.channels.filterPredicate = (rowData: Channel, fltr: string) => {
let rowToFilter = '';
switch (this.selFilterBy) {
case 'all':
rowToFilter = (rowData.alias ? rowData.alias.toLowerCase() : '') +
rowData.htlcs?.map((htlc) => JSON.stringify(htlc).toLowerCase() + (htlc.local_trimmed ? ' yes ' : ' no '));
break;
case 'direction':
rowToFilter = rowData.htlcs?.map((htlc) => htlc.direction + ' ').toString() || '';
break;
case 'id':
rowToFilter = rowData.htlcs?.map((htlc) => htlc.id + ' ').toString() || '';
break;
case 'expiry':
rowToFilter = rowData.htlcs?.map((htlc) => htlc.expiry + ' ').toString() || '';
break;
case 'state':
rowToFilter = rowData.htlcs?.map((htlc) => this.camelCaseWithReplace.transform((htlc.state || '') || '', '_').toLowerCase() + ' ').toString() || '';
break;
case 'payment_hash':
rowToFilter = rowData.htlcs?.map((htlc) => htlc.payment_hash + ' ').toString() || '';
break;
case 'local_trimmed':
rowToFilter = rowData.htlcs?.map((htlc) => (htlc.local_trimmed ? ' yes ' : ' no ')).toString() || '';
break;
case 'amount_msat':
rowToFilter = (rowData.htlcs?.map((htlc) => (htlc.amount_msat || 0) / 1000))?.toString() || '';
break;
default:
rowToFilter = typeof rowData[this.selFilterBy] === 'undefined' ? '' : typeof rowData[this.selFilterBy] === 'string' ? rowData[this.selFilterBy].toLowerCase() : typeof rowData[this.selFilterBy] === 'boolean' ? (rowData[this.selFilterBy] ? 'yes' : 'no') : rowData[this.selFilterBy].toString();
break;
}
return rowToFilter.includes(fltr);
};
}
loadHTLCsTable(channels: Channel[]) {
this.channels = (channels) ? new MatTableDataSource<Channel>([...channels]) : new MatTableDataSource([]);
this.channels.sort = this.sort;
this.channels.sortingDataAccessor = (data: any, sortHeaderId: string) => {
switch (sortHeaderId) {
case 'amount_msat':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'number', this.sort?.direction);
return data.htlcs && data.htlcs.length ? data.htlcs.length : null;
case 'id':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'string', this.sort?.direction);
return data;
case 'direction':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'string', this.sort?.direction);
return data.alias ? data.alias : data.id ? data.id : null;
case 'expiry':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'number', this.sort?.direction);
return data;
case 'payment_hash':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'string', this.sort?.direction);
return data;
case 'state':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'string', this.sort?.direction);
return data;
case 'local_trimmed':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'boolean', this.sort?.direction);
return data;
default:
return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null;
}
};
this.channels.paginator = this.paginator;
this.setFilterPredicate();
this.applyFilter();
}
onDownloadCSV() {
if (this.channels.data && this.channels.data.length > 0) {
this.commonService.downloadFile(this.flattenHTLCs(), 'ActiveHTLCs');
}
}
flattenHTLCs() {
const channelsDataCopy = JSON.parse(JSON.stringify(this.channels.data));
const flattenedHTLCs = channelsDataCopy?.reduce((acc, curr) => {
if (curr.htlcs) {
return acc.concat(curr.htlcs);
} else {
return acc.concat(curr);
}
}, []);
return flattenedHTLCs;
}
ngOnDestroy() {
this.unSubs.forEach((completeSub) => {
completeSub.next(<any>null);
completeSub.complete();
});
}
}

@ -71,32 +71,32 @@
<ng-container matColumnDef="our_channel_reserve_satoshis"> <ng-container matColumnDef="our_channel_reserve_satoshis">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Local Reserve (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Local Reserve (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.our_channel_reserve_satoshis | number:'1.0-0'}} </span></td> {{channel?.our_reserve_msat / 1000 | number:'1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="their_channel_reserve_satoshis"> <ng-container matColumnDef="their_channel_reserve_satoshis">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Remote Reserve (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Remote Reserve (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.their_channel_reserve_satoshis | number:'1.0-0'}} </span></td> {{channel?.their_reserve_msat / 1000 | number:'1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="msatoshi_total"> <ng-container matColumnDef="msatoshi_total">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Total (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Total (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.msatoshi_total/1000 | number:channel?.msatoshi_to_us < 1000 ? '1.0-4' : '1.0-0'}} </span></td> {{channel?.total_msat/1000 | number:channel?.to_us_msat < 1000 ? '1.0-4' : '1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="spendable_msatoshi"> <ng-container matColumnDef="spendable_msatoshi">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Spendable (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Spendable (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.spendable_msatoshi/1000 | number:channel?.msatoshi_to_us < 1000 ? '1.0-4' : '1.0-0'}} </span></td> {{channel?.spendable_msat/1000 | number:channel?.to_us_msat < 1000 ? '1.0-4' : '1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="msatoshi_to_us"> <ng-container matColumnDef="msatoshi_to_us">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Local Balance (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Local Balance (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.msatoshi_to_us/1000 | number:channel?.msatoshi_to_us < 1000 ? '1.0-4' : '1.0-0'}} </span></td> {{channel?.to_us_msat/1000 | number:channel?.to_us_msat < 1000 ? '1.0-4' : '1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="msatoshi_to_them"> <ng-container matColumnDef="msatoshi_to_them">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Remote Balance (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Remote Balance (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.msatoshi_to_them/1000 | number:channel?.msatoshi_to_them < 1000 ? '1.0-4' : '1.0-0'}} </span></td> {{channel?.to_them_msat/1000 | number:channel?.to_them_msat < 1000 ? '1.0-4' : '1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="balancedness"> <ng-container matColumnDef="balancedness">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Balance Score</th> <th *matHeaderCellDef mat-header-cell mat-sort-header>Balance Score</th>
@ -104,7 +104,7 @@
<div fxLayout="row"> <div fxLayout="row">
<mat-hint fxFlex="100" fxLayoutAlign="center center" class="font-size-80">{{channel.balancedness || 0 | number}}</mat-hint> <mat-hint fxFlex="100" fxLayoutAlign="center center" class="font-size-80">{{channel.balancedness || 0 | number}}</mat-hint>
</div> </div>
<mat-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 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>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">

@ -1,5 +1,4 @@
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators'; import { take, takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -64,12 +63,15 @@ export class CLNChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
public apiCallStatusEnum = APICallStatusEnum; public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<RTLState>, private rtlEffects: RTLEffects, private clnEffects: CLNEffects, private commonService: CommonService, private router: Router, private camelCaseWithReplace: CamelCaseWithReplacePipe) { constructor(private logger: LoggerService, private store: Store<RTLState>, private rtlEffects: RTLEffects, private clnEffects: CLNEffects, private commonService: CommonService, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.screenSize = this.commonService.getScreenSize(); this.screenSize = this.commonService.getScreenSize();
this.selFilter = this.router?.getCurrentNavigation()?.extras?.state?.filter ? this.router?.getCurrentNavigation()?.extras?.state?.filter : '';
} }
ngOnInit() { ngOnInit() {
if (window.history.state && window.history.state.filterColumn) {
this.selFilterBy = window.history.state.filterColumn || 'all';
this.selFilter = window.history.state.filterValue || '';
}
this.store.select(nodeInfoAndBalanceAndNumPeers).pipe(takeUntil(this.unSubs[0])). this.store.select(nodeInfoAndBalanceAndNumPeers).pipe(takeUntil(this.unSubs[0])).
subscribe((infoBalNumpeersSelector: { information: GetInfo, balance: Balance, numPeers: number }) => { subscribe((infoBalNumpeersSelector: { information: GetInfo, balance: Balance, numPeers: number }) => {
this.information = infoBalNumpeersSelector.information; this.information = infoBalNumpeersSelector.information;
@ -155,7 +157,6 @@ export class CLNChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
return; return;
} }
if (channelToUpdate === 'all') { if (channelToUpdate === 'all') {
const confirmationMsg = [];
this.store.dispatch(openConfirmation({ this.store.dispatch(openConfirmation({
payload: { payload: {
data: { data: {
@ -163,7 +164,7 @@ export class CLNChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
alertTitle: 'Update Fee Policy', alertTitle: 'Update Fee Policy',
noBtnText: 'Cancel', noBtnText: 'Cancel',
yesBtnText: 'Update All', yesBtnText: 'Update All',
message: confirmationMsg, message: [],
titleMessage: 'Update fee policy for all channels', titleMessage: 'Update fee policy for all channels',
flgShowInput: true, flgShowInput: true,
getInputs: [ getInputs: [
@ -274,7 +275,7 @@ export class CLNChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
getLabel(column: string) { getLabel(column: string) {
const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column); const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column);
return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform(returnColumn.column, '_') : this.commonService.titleCase(column); return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform((returnColumn.column || ''), '_') : this.commonService.titleCase(column);
} }
setFilterPredicate() { setFilterPredicate() {
@ -282,12 +283,13 @@ export class CLNChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
let rowToFilter = ''; let rowToFilter = '';
switch (this.selFilterBy) { switch (this.selFilterBy) {
case 'all': case 'all':
rowToFilter = ((rowData.connected) ? 'connected' : 'disconnected') + (rowData.channel_id ? rowData.channel_id.toLowerCase() : '') + rowToFilter = ((rowData.peer_connected) ? 'connected' : 'disconnected') + (rowData.channel_id ? rowData.channel_id.toLowerCase() : '') +
(rowData.short_channel_id ? rowData.short_channel_id.toLowerCase() : '') + (rowData.id ? rowData.id.toLowerCase() : '') + (rowData.alias ? rowData.alias.toLowerCase() : '') + (rowData.short_channel_id ? rowData.short_channel_id.toLowerCase() : '') + (rowData.id ? rowData.id.toLowerCase() : '') + (rowData.alias ? rowData.alias.toLowerCase() : '') +
(rowData.private ? 'private' : 'public') + (rowData.state ? rowData.state.toLowerCase() : '') + (rowData.private ? 'private' : 'public') + (rowData.state ? rowData.state.toLowerCase() : '') +
(rowData.funding_txid ? rowData.funding_txid.toLowerCase() : '') + (rowData.msatoshi_to_us ? rowData.msatoshi_to_us : '') + (rowData.funding_txid ? rowData.funding_txid.toLowerCase() : '') + (rowData.to_them_msat ? rowData.to_them_msat / 1000 : '') +
(rowData.msatoshi_total ? rowData.msatoshi_total : '') + (rowData.their_channel_reserve_satoshis ? rowData.their_channel_reserve_satoshis : '') + (rowData.to_us_msat ? rowData.to_us_msat / 1000 : '') + (rowData.total_msat ? rowData.total_msat / 1000 : '') +
(rowData.our_channel_reserve_satoshis ? rowData.our_channel_reserve_satoshis : '') + (rowData.spendable_msatoshi ? rowData.spendable_msatoshi : ''); (rowData.their_reserve_msat ? rowData.their_reserve_msat / 1000 : '') + (rowData.our_reserve_msat ? rowData.our_reserve_msat / 1000 : '') +
(rowData.spendable_msat ? rowData.spendable_msat / 1000 : '');
break; break;
case 'private': case 'private':
@ -295,14 +297,31 @@ export class CLNChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
break; break;
case 'connected': case 'connected':
rowToFilter = rowData?.connected ? 'connected' : 'disconnected'; rowToFilter = rowData?.peer_connected ? 'connected' : 'disconnected';
break; break;
case 'msatoshi_total': case 'msatoshi_total':
rowToFilter = ((rowData['total_msat'] || 0) / 1000)?.toString() || '';
break;
case 'spendable_msatoshi': case 'spendable_msatoshi':
rowToFilter = ((rowData['spendable_msat'] || 0) / 1000)?.toString() || '';
break;
case 'msatoshi_to_us': case 'msatoshi_to_us':
rowToFilter = ((rowData['to_us_msat'] || 0) / 1000)?.toString() || '';
break;
case 'msatoshi_to_them': case 'msatoshi_to_them':
rowToFilter = ((+(rowData[this.selFilterBy] || 0)) / 1000)?.toString() || ''; rowToFilter = ((rowData['to_them_msat'] || 0) / 1000)?.toString() || '';
break;
case 'our_channel_reserve_satoshis':
rowToFilter = ((rowData['our_reserve_msat'] || 0) / 1000)?.toString() || '';
break;
case 'their_channel_reserve_satoshis':
rowToFilter = ((rowData['their_reserve_msat'] || 0) / 1000)?.toString() || '';
break; break;
default: default:
@ -316,7 +335,30 @@ export class CLNChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
loadChannelsTable(mychannels) { loadChannelsTable(mychannels) {
this.channels = new MatTableDataSource<Channel>([...mychannels]); this.channels = new MatTableDataSource<Channel>([...mychannels]);
this.channels.sort = this.sort; this.channels.sort = this.sort;
this.channels.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null); this.channels.sortingDataAccessor = (data: any, sortHeaderId: string) => {
switch (sortHeaderId) {
case 'msatoshi_total':
return data['total_msat'];
case 'spendable_msatoshi':
return data['spendable_msat'];
case 'msatoshi_to_us':
return data['to_us_msat'];
case 'msatoshi_to_them':
return data['to_them_msat'];
case 'our_channel_reserve_satoshis':
return data['our_reserve_msat'];
case 'their_channel_reserve_satoshis':
return data['their_reserve_msat'];
default:
return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null;
}
};
this.channels.paginator = this.paginator; this.channels.paginator = this.paginator;
this.setFilterPredicate(); this.setFilterPredicate();
this.applyFilter(); this.applyFilter();

@ -67,32 +67,32 @@
<ng-container matColumnDef="our_channel_reserve_satoshis"> <ng-container matColumnDef="our_channel_reserve_satoshis">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Local Reserve (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Local Reserve (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.our_channel_reserve_satoshis | number:'1.0-0'}} </span></td> {{channel?.our_reserve_msat/1000 | number:'1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="their_channel_reserve_satoshis"> <ng-container matColumnDef="their_channel_reserve_satoshis">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Remote Reserve (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Remote Reserve (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.their_channel_reserve_satoshis | number:'1.0-0'}} </span></td> {{channel?.their_reserve_msat/1000 | number:'1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="msatoshi_total"> <ng-container matColumnDef="msatoshi_total">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Total (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Total (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.msatoshi_total/1000 | number:channel?.msatoshi_to_us < 1000 ? '1.0-4' : '1.0-0'}} </span></td> {{channel?.total_msat/1000 | number:channel?.to_us_msat < 1000 ? '1.0-4' : '1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="spendable_msatoshi"> <ng-container matColumnDef="spendable_msatoshi">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Spendable (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Spendable (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.spendable_msatoshi/1000 | number:channel?.msatoshi_to_us < 1000 ? '1.0-4' : '1.0-0'}} </span></td> {{channel?.spendable_msat/1000 | number:channel?.to_us_msat < 1000 ? '1.0-4' : '1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="msatoshi_to_us"> <ng-container matColumnDef="msatoshi_to_us">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Local Balance (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Local Balance (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.msatoshi_to_us/1000 | number:channel?.msatoshi_to_us < 1000 ? '1.0-4' : '1.0-0'}} </span></td> {{channel?.to_us_msat/1000 | number:channel?.to_us_msat < 1000 ? '1.0-4' : '1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="msatoshi_to_them"> <ng-container matColumnDef="msatoshi_to_them">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Remote Balance (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Remote Balance (Sats)</th>
<td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center"> <td *matCellDef="let channel" mat-cell><span fxLayoutAlign="end center">
{{channel?.msatoshi_to_them/1000 | number:channel?.msatoshi_to_them < 1000 ? '1.0-4' : '1.0-0'}} </span></td> {{channel?.to_them_msat/1000 | number:channel?.to_them_msat < 1000 ? '1.0-4' : '1.0-0'}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th *matHeaderCellDef mat-header-cell> <th *matHeaderCellDef mat-header-cell>
@ -108,7 +108,7 @@
<mat-select placeholder="Actions" tabindex="4" class="mr-0"> <mat-select placeholder="Actions" tabindex="4" class="mr-0">
<mat-select-trigger></mat-select-trigger> <mat-select-trigger></mat-select-trigger>
<mat-option (click)="onChannelClick(channel, $event)">View Info</mat-option> <mat-option (click)="onChannelClick(channel, $event)">View Info</mat-option>
<mat-option *ngIf="isCompatibleVersion && (channel.state === 'CHANNELD_SHUTTING_DOWN' || channel.state === 'CLOSINGD_SIGEXCHANGE' || (!channel.connected && channel.state === 'CHANNELD_NORMAL'))" (click)="onChannelClose(channel)">Close Channel</mat-option> <mat-option *ngIf="(channel.state === 'CHANNELD_SHUTTING_DOWN' || channel.state === 'CLOSINGD_SIGEXCHANGE' || (!channel.connected && channel.state === 'CHANNELD_NORMAL'))" (click)="onChannelClose(channel)">Close Channel</mat-option>
<mat-option *ngIf="channel.state === 'CHANNELD_AWAITING_LOCKIN'" (click)="onBumpFee(channel)">Bump Fee</mat-option> <mat-option *ngIf="channel.state === 'CHANNELD_AWAITING_LOCKIN'" (click)="onBumpFee(channel)">Bump Fee</mat-option>
</mat-select> </mat-select>
</div> </div>

@ -44,7 +44,6 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
public colWidth = '20rem'; public colWidth = '20rem';
public PAGE_ID = 'peers_channels'; public PAGE_ID = 'peers_channels';
public tableSetting: TableSetting = { tableId: 'pending_inactive_channels', recordsPerPage: PAGE_SIZE, sortBy: 'alias', sortOrder: SortOrderEnum.DESCENDING }; public tableSetting: TableSetting = { tableId: 'pending_inactive_channels', recordsPerPage: PAGE_SIZE, sortBy: 'alias', sortOrder: SortOrderEnum.DESCENDING };
public isCompatibleVersion = false;
public totalBalance = 0; public totalBalance = 0;
public displayedColumns: any[] = []; public displayedColumns: any[] = [];
public channelsData: Channel[] = []; public channelsData: Channel[] = [];
@ -72,9 +71,6 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
this.store.select(nodeInfoAndBalanceAndNumPeers).pipe(takeUntil(this.unSubs[0])). this.store.select(nodeInfoAndBalanceAndNumPeers).pipe(takeUntil(this.unSubs[0])).
subscribe((infoBalNumpeersSelector: { information: GetInfo, balance: Balance, numPeers: number }) => { subscribe((infoBalNumpeersSelector: { information: GetInfo, balance: Balance, numPeers: number }) => {
this.information = infoBalNumpeersSelector.information; this.information = infoBalNumpeersSelector.information;
if (this.information.api_version) {
this.isCompatibleVersion = this.commonService.isVersionCompatible(this.information.api_version, '0.4.2');
}
this.numPeers = infoBalNumpeersSelector.numPeers; this.numPeers = infoBalNumpeersSelector.numPeers;
this.totalBalance = infoBalNumpeersSelector.balance.totalBalance || 0; this.totalBalance = infoBalNumpeersSelector.balance.totalBalance || 0;
this.logger.info(infoBalNumpeersSelector); this.logger.info(infoBalNumpeersSelector);
@ -172,7 +168,7 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
getLabel(column: string) { getLabel(column: string) {
const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column); const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column);
return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform(returnColumn.column, '_') : this.commonService.titleCase(column); return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform((returnColumn.column || ''), '_') : this.commonService.titleCase(column);
} }
setFilterPredicate() { setFilterPredicate() {
@ -180,12 +176,12 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
let rowToFilter = ''; let rowToFilter = '';
switch (this.selFilterBy) { switch (this.selFilterBy) {
case 'all': case 'all':
rowToFilter = ((rowData.connected) ? 'connected' : 'disconnected') + (rowData.channel_id ? rowData.channel_id.toLowerCase() : '') + rowToFilter = ((rowData.peer_connected) ? 'connected' : 'disconnected') + (rowData.channel_id ? rowData.channel_id.toLowerCase() : '') +
(rowData.short_channel_id ? rowData.short_channel_id.toLowerCase() : '') + (rowData.id ? rowData.id.toLowerCase() : '') + (rowData.alias ? rowData.alias.toLowerCase() : '') + (rowData.short_channel_id ? rowData.short_channel_id.toLowerCase() : '') + (rowData.id ? rowData.id.toLowerCase() : '') + (rowData.alias ? rowData.alias.toLowerCase() : '') +
(rowData.private ? 'private' : 'public') + ((rowData.state && this.CLNChannelPendingState[rowData.state]) ? this.CLNChannelPendingState[rowData.state].toLowerCase() : '') + (rowData.private ? 'private' : 'public') + ((rowData.state && this.CLNChannelPendingState[rowData.state]) ? this.CLNChannelPendingState[rowData.state].toLowerCase() : '') +
(rowData.funding_txid ? rowData.funding_txid.toLowerCase() : '') + (rowData.msatoshi_to_us ? rowData.msatoshi_to_us : '') + (rowData.funding_txid ? rowData.funding_txid.toLowerCase() : '') + (rowData.to_us_msat ? rowData.to_us_msat : '') + (rowData.to_them_msat ? rowData.to_them_msat / 1000 : '') +
(rowData.msatoshi_total ? rowData.msatoshi_total : '') + (rowData.their_channel_reserve_satoshis ? rowData.their_channel_reserve_satoshis : '') + (rowData.total_msat ? rowData.total_msat / 1000 : '') + (rowData.their_reserve_msat ? rowData.their_reserve_msat / 1000 : '') +
(rowData.our_channel_reserve_satoshis ? rowData.our_channel_reserve_satoshis : '') + (rowData.spendable_msatoshi ? rowData.spendable_msatoshi : ''); (rowData.our_reserve_msat ? rowData.our_reserve_msat / 1000 : '') + (rowData.spendable_msat ? rowData.spendable_msat / 1000 : '');
break; break;
case 'private': case 'private':
@ -193,14 +189,31 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
break; break;
case 'connected': case 'connected':
rowToFilter = rowData?.connected ? 'connected' : 'disconnected'; rowToFilter = rowData?.peer_connected ? 'connected' : 'disconnected';
break; break;
case 'msatoshi_total': case 'msatoshi_total':
rowToFilter = ((rowData['total_msat'] || 0) / 1000)?.toString() || '';
break;
case 'spendable_msatoshi': case 'spendable_msatoshi':
rowToFilter = ((rowData['spendable_msat'] || 0) / 1000)?.toString() || '';
break;
case 'msatoshi_to_us': case 'msatoshi_to_us':
rowToFilter = ((rowData['to_us_msat'] || 0) / 1000)?.toString() || '';
break;
case 'msatoshi_to_them': case 'msatoshi_to_them':
rowToFilter = ((+(rowData[this.selFilterBy] || 0)) / 1000)?.toString() || ''; rowToFilter = ((rowData['to_them_msat'] || 0) / 1000)?.toString() || '';
break;
case 'our_channel_reserve_satoshis':
rowToFilter = ((rowData['our_reserve_msat'] || 0) / 1000)?.toString() || '';
break;
case 'their_channel_reserve_satoshis':
rowToFilter = ((rowData['their_reserve_msat'] || 0) / 1000)?.toString() || '';
break; break;
case 'state': case 'state':
@ -220,6 +233,24 @@ export class CLNChannelPendingTableComponent implements OnInit, AfterViewInit, O
this.channels.sort = this.sort; this.channels.sort = this.sort;
this.channels.sortingDataAccessor = (data: any, sortHeaderId: string) => { this.channels.sortingDataAccessor = (data: any, sortHeaderId: string) => {
switch (sortHeaderId) { switch (sortHeaderId) {
case 'msatoshi_total':
return data['total_msat'];
case 'spendable_msatoshi':
return data['spendable_msat'];
case 'msatoshi_to_us':
return data['to_us_msat'];
case 'msatoshi_to_them':
return data['to_them_msat'];
case 'our_channel_reserve_satoshis':
return data['our_reserve_msat'];
case 'their_channel_reserve_satoshis':
return data['their_reserve_msat'];
case 'state': case 'state':
return this.CLNChannelPendingState[data.state]; return this.CLNChannelPendingState[data.state];

@ -14,6 +14,11 @@
<span matBadgeOverlap="false" class="tab-badge" matBadge="{{pendingChannels}}">Pending/Inactive</span> <span matBadgeOverlap="false" class="tab-badge" matBadge="{{pendingChannels}}">Pending/Inactive</span>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<span matBadgeOverlap="false" class="tab-badge" matBadge="{{activeHTLCs}}">Active HTLCs</span>
</ng-template>
</mat-tab>
</mat-tab-group> </mat-tab-group>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large"> <div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large">
<router-outlet></router-outlet> <router-outlet></router-outlet>

@ -24,12 +24,13 @@ export class CLNChannelsTablesComponent implements OnInit, OnDestroy {
public openChannels = 0; public openChannels = 0;
public pendingChannels = 0; public pendingChannels = 0;
public activeHTLCs = 0;
public selNode: SelNodeChild | null = {}; public selNode: SelNodeChild | null = {};
public information: GetInfo = {}; public information: GetInfo = {};
public peers: Peer[] = []; public peers: Peer[] = [];
public utxos: UTXO[] = []; public utxos: UTXO[] = [];
public totalBalance = 0; public totalBalance = 0;
public links = [{ link: 'open', name: 'Open' }, { link: 'pending', name: 'Pending/Inactive' }]; public links = [{ link: 'open', name: 'Open' }, { link: 'pending', name: 'Pending/Inactive' }, { link: 'activehtlcs', name: 'Active HTLCs' }];
public activeLink = 0; public activeLink = 0;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
@ -51,18 +52,20 @@ export class CLNChannelsTablesComponent implements OnInit, OnDestroy {
this.logger.info(infoSettingsBalSelector); this.logger.info(infoSettingsBalSelector);
}); });
this.store.select(peers).pipe(takeUntil(this.unSubs[2])). this.store.select(peers).pipe(takeUntil(this.unSubs[2])).
subscribe((peersSeletor: { peers: Peer[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((peersSelector: { peers: Peer[], apiCallStatus: ApiCallStatusPayload }) => {
this.peers = peersSeletor.peers; this.peers = peersSelector.peers;
}); });
this.store.select(utxos).pipe(takeUntil(this.unSubs[3])). this.store.select(utxos).pipe(takeUntil(this.unSubs[3])).
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
this.utxos = this.commonService.sortAscByKey(utxosSeletor.utxos?.filter((utxo) => utxo.status === 'confirmed'), 'value'); this.utxos = this.commonService.sortAscByKey(utxosSeletor.utxos?.filter((utxo) => utxo.status === 'confirmed'), 'value');
}); });
this.store.select(channels).pipe(takeUntil(this.unSubs[4])). this.store.select(channels).pipe(takeUntil(this.unSubs[4])).
subscribe((channelsSeletor: { activeChannels: Channel[], pendingChannels: Channel[], inactiveChannels: Channel[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((channelsSelector: { activeChannels: Channel[], pendingChannels: Channel[], inactiveChannels: Channel[], apiCallStatus: ApiCallStatusPayload }) => {
this.openChannels = channelsSeletor.activeChannels.length || 0; this.openChannels = channelsSelector.activeChannels.length || 0;
this.pendingChannels = (channelsSeletor.pendingChannels.length + channelsSeletor.inactiveChannels.length) || 0; this.pendingChannels = (channelsSelector.pendingChannels.length + channelsSelector.inactiveChannels.length) || 0;
this.logger.info(channelsSeletor); const allChannels = [...channelsSelector.activeChannels, ...channelsSelector.pendingChannels, ...channelsSelector.inactiveChannels];
this.activeHTLCs = allChannels?.reduce((totalHTLCs, peer) => totalHTLCs + (peer.htlcs && peer.htlcs.length > 0 ? peer.htlcs.length : 0), 0);
this.logger.info(channelsSelector);
}); });
} }
@ -71,9 +74,7 @@ export class CLNChannelsTablesComponent implements OnInit, OnDestroy {
peers: this.peers, peers: this.peers,
information: this.information, information: this.information,
balance: this.totalBalance, balance: this.totalBalance,
utxos: this.utxos, utxos: this.utxos
isCompatibleVersion: this.commonService.isVersionCompatible(this.information.version, '0.9.0') &&
this.commonService.isVersionCompatible(this.information.api_version, '0.4.0')
}; };
this.store.dispatch(openAlert({ this.store.dispatch(openAlert({
payload: { payload: {

@ -66,20 +66,18 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="isCompatibleVersion" fxLayout="column" fxLayoutAlign="space-between stretch" fxLayoutAlign.gt-sm="space-between center" fxLayout.gt-sm="row wrap"> <mat-form-field fxLayout="column" fxFlex="54" fxLayoutAlign="start end">
<mat-form-field fxLayout="column" fxFlex="54" fxLayoutAlign="start end"> <mat-label>Coin Selection</mat-label>
<mat-label>Coin Selection</mat-label> <mat-select tabindex="6" multiple [(value)]="selUTXOs" (selectionChange)="onUTXOSelectionChange($event)">
<mat-select tabindex="6" multiple [(value)]="selUTXOs" (selectionChange)="onUTXOSelectionChange($event)"> <mat-select-trigger>{{totalSelectedUTXOAmount | number}} Sats ({{selUTXOs.length > 1 ? selUTXOs.length + ' UTXOs' : '1 UTXO'}})</mat-select-trigger>
<mat-select-trigger>{{totalSelectedUTXOAmount | number}} Sats ({{selUTXOs.length > 1 ? selUTXOs.length + ' UTXOs' : '1 UTXO'}})</mat-select-trigger> <mat-option *ngFor="let utxo of utxos" [value]="utxo">{{(utxo.amount_msat) / 1000 | number:'1.0-0'}} Sats</mat-option>
<mat-option *ngFor="let utxo of utxos" [value]="utxo">{{utxo.value | number}} Sats</mat-option> </mat-select>
</mat-select> </mat-form-field>
</mat-form-field> <div fxFlex="41" fxLayout="row" fxLayoutAlign="start center">
<div fxFlex="41" fxLayout="row" fxLayoutAlign="start center"> <mat-slide-toggle tabindex="7" color="primary" name="flgUseAllBalance" [disabled]="selUTXOs.length < 1" [(ngModel)]="flgUseAllBalance" (change)="onUTXOAllBalanceChange()">
<mat-slide-toggle tabindex="7" color="primary" name="flgUseAllBalance" [disabled]="selUTXOs.length < 1" [(ngModel)]="flgUseAllBalance" (change)="onUTXOAllBalanceChange()"> Use selected UTXOs balance
Use selected UTXOs balance </mat-slide-toggle>
</mat-slide-toggle> <mat-icon matTooltip="Use selected UTXOs balance as the amount to be sent. Final amount sent will be less the mining fee." matTooltipPosition="before" class="info-icon">info_outline</mat-icon>
<mat-icon matTooltip="Use selected UTXOs balance as the amount to be sent. Final amount sent will be less the mining fee." matTooltipPosition="before" class="info-icon">info_outline</mat-icon>
</div>
</div> </div>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>

@ -29,7 +29,6 @@ export class CLNOpenChannelComponent implements OnInit, OnDestroy {
public selectedPeer = new UntypedFormControl(); public selectedPeer = new UntypedFormControl();
public faExclamationTriangle = faExclamationTriangle; public faExclamationTriangle = faExclamationTriangle;
public alertTitle: string; public alertTitle: string;
public isCompatibleVersion = false;
public selNode: SelNodeChild | null = {}; public selNode: SelNodeChild | null = {};
public peer: Peer | null; public peer: Peer | null;
public peers: Peer[]; public peers: Peer[];
@ -61,14 +60,12 @@ export class CLNOpenChannelComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
if (this.data.message) { if (this.data.message) {
this.isCompatibleVersion = this.data.message.isCompatibleVersion;
this.information = this.data.message.information; this.information = this.data.message.information;
this.totalBalance = this.data.message.balance; this.totalBalance = this.data.message.balance;
this.utxos = this.data.message.utxos; this.utxos = this.data.message.utxos;
this.peer = this.data.message.peer || null; this.peer = this.data.message.peer || null;
this.peers = this.data.message.peers || []; this.peers = this.data.message.peers || [];
} else { } else {
this.isCompatibleVersion = false;
this.information = {}; this.information = {};
this.totalBalance = 0; this.totalBalance = 0;
this.utxos = []; this.utxos = [];
@ -168,7 +165,7 @@ export class CLNOpenChannelComponent implements OnInit, OnDestroy {
onUTXOSelectionChange(event: any) { onUTXOSelectionChange(event: any) {
if (this.selUTXOs.length && this.selUTXOs.length > 0) { if (this.selUTXOs.length && this.selUTXOs.length > 0) {
this.totalSelectedUTXOAmount = this.selUTXOs?.reduce((acc, curr: UTXO) => acc + (curr.value || 0), 0); this.totalSelectedUTXOAmount = this.selUTXOs?.reduce((acc, curr: UTXO) => acc + ((curr.amount_msat || 0) / 1000), 0);
if (this.flgUseAllBalance) { if (this.flgUseAllBalance) {
this.onUTXOAllBalanceChange(); this.onUTXOAllBalanceChange();
} }

@ -131,6 +131,9 @@ export class CLNPeersComponent implements OnInit, AfterViewInit, OnDestroy {
data: { data: {
type: AlertTypeEnum.INFORMATION, type: AlertTypeEnum.INFORMATION,
alertTitle: 'Peer Information', alertTitle: 'Peer Information',
goToFieldValue: selPeer.id,
goToName: 'Graph lookup',
goToLink: '/cln/graph/lookups',
showQRName: 'Public Key', showQRName: 'Public Key',
showQRField: selPeer.id, showQRField: selPeer.id,
message: reorderedPeer message: reorderedPeer

@ -132,9 +132,9 @@ export class CLNRoutingReportComponent implements OnInit, OnDestroy {
} }
this.filteredEventsBySelectedPeriod?.map((event) => { this.filteredEventsBySelectedPeriod?.map((event) => {
const monthNumber = event.received_time ? new Date((+event.received_time) * 1000).getMonth() : 12; const monthNumber = event.received_time ? new Date((+event.received_time) * 1000).getMonth() : 12;
feeReport[monthNumber].value = event.fee ? feeReport[monthNumber].value + (+event.fee / 1000) : feeReport[monthNumber].value;
feeReport[monthNumber].extra.totalEvents = feeReport[monthNumber].extra.totalEvents + 1; feeReport[monthNumber].extra.totalEvents = feeReport[monthNumber].extra.totalEvents + 1;
this.totalFeeMsat = event.fee ? (this.totalFeeMsat ? this.totalFeeMsat : 0) + +event.fee : this.totalFeeMsat; feeReport[monthNumber].value = feeReport[monthNumber].value + (+(event.fee_msat || 0) / 1000);
this.totalFeeMsat = (this.totalFeeMsat || 0) + +(event.fee_msat || 0);
return this.filteredEventsBySelectedPeriod; return this.filteredEventsBySelectedPeriod;
}); });
} else { } else {
@ -143,9 +143,9 @@ export class CLNRoutingReportComponent implements OnInit, OnDestroy {
} }
this.filteredEventsBySelectedPeriod?.map((event) => { this.filteredEventsBySelectedPeriod?.map((event) => {
const dateNumber = event.received_time ? Math.floor((+event.received_time - startDateInSeconds) / this.secondsInADay) : 0; const dateNumber = event.received_time ? Math.floor((+event.received_time - startDateInSeconds) / this.secondsInADay) : 0;
feeReport[dateNumber].value = event.fee ? feeReport[dateNumber].value + (+event.fee / 1000) : feeReport[dateNumber].value;
feeReport[dateNumber].extra.totalEvents = feeReport[dateNumber].extra.totalEvents + 1; feeReport[dateNumber].extra.totalEvents = feeReport[dateNumber].extra.totalEvents + 1;
this.totalFeeMsat = event.fee ? (this.totalFeeMsat ? this.totalFeeMsat : 0) + +event.fee : this.totalFeeMsat; feeReport[dateNumber].value = feeReport[dateNumber].value + (+(event.fee_msat || 0) / 1000);
this.totalFeeMsat = (this.totalFeeMsat || 0) + +(event.fee_msat || 0);
return this.filteredEventsBySelectedPeriod; return this.filteredEventsBySelectedPeriod;
}); });
} }
@ -163,8 +163,8 @@ export class CLNRoutingReportComponent implements OnInit, OnDestroy {
this.filteredEventsBySelectedPeriod?.map((event) => { this.filteredEventsBySelectedPeriod?.map((event) => {
const monthNumber = event.received_time ? new Date((+event.received_time) * 1000).getMonth() : 12; const monthNumber = event.received_time ? new Date((+event.received_time) * 1000).getMonth() : 12;
eventsReport[monthNumber].value = eventsReport[monthNumber].value + 1; eventsReport[monthNumber].value = eventsReport[monthNumber].value + 1;
eventsReport[monthNumber].extra.totalFees = event.fee ? eventsReport[monthNumber].extra.totalFees + (+event.fee / 1000) : eventsReport[monthNumber].extra.totalFees; eventsReport[monthNumber].extra.totalFees = eventsReport[monthNumber].extra.totalFees + (+(event.fee_msat || 0) / 1000);
this.totalFeeMsat = event.fee ? (this.totalFeeMsat ? this.totalFeeMsat : 0) + +event.fee : this.totalFeeMsat; this.totalFeeMsat = (this.totalFeeMsat || 0) + +(event.fee_msat || 0);
return this.filteredEventsBySelectedPeriod; return this.filteredEventsBySelectedPeriod;
}); });
} else { } else {
@ -174,8 +174,8 @@ export class CLNRoutingReportComponent implements OnInit, OnDestroy {
this.filteredEventsBySelectedPeriod?.map((event) => { this.filteredEventsBySelectedPeriod?.map((event) => {
const dateNumber = event.received_time ? Math.floor((+event.received_time - startDateInSeconds) / this.secondsInADay) : 0; const dateNumber = event.received_time ? Math.floor((+event.received_time - startDateInSeconds) / this.secondsInADay) : 0;
eventsReport[dateNumber].value = eventsReport[dateNumber].value + 1; eventsReport[dateNumber].value = eventsReport[dateNumber].value + 1;
eventsReport[dateNumber].extra.totalFees = event.fee ? eventsReport[dateNumber].extra.totalFees + (+event.fee / 1000) : eventsReport[dateNumber].extra.totalFees; eventsReport[dateNumber].extra.totalFees = eventsReport[dateNumber].extra.totalFees + (+(event.fee_msat || 0) / 1000);
this.totalFeeMsat = event.fee ? (this.totalFeeMsat ? this.totalFeeMsat : 0) + +event.fee : this.totalFeeMsat; this.totalFeeMsat = (this.totalFeeMsat || 0) + +(event.fee_msat || 0);
return this.filteredEventsBySelectedPeriod; return this.filteredEventsBySelectedPeriod;
}); });
} }

@ -122,15 +122,15 @@ export class CLNTransactionsReportComponent implements OnInit, OnDestroy {
} }
filteredPayments?.map((payment) => { filteredPayments?.map((payment) => {
const monthNumber = new Date((payment.created_at || 0) * 1000).getMonth(); const monthNumber = new Date((payment.created_at || 0) * 1000).getMonth();
this.transactionsReportSummary.amountPaidSelectedPeriod = this.transactionsReportSummary.amountPaidSelectedPeriod + (payment.msatoshi_sent || 0); this.transactionsReportSummary.amountPaidSelectedPeriod = this.transactionsReportSummary.amountPaidSelectedPeriod + (payment.amount_sent_msat || 0);
transactionsReport[monthNumber].series[0].value = transactionsReport[monthNumber].series[0].value + ((payment.msatoshi_sent || 0) / 1000); transactionsReport[monthNumber].series[0].value = transactionsReport[monthNumber].series[0].value + ((payment.amount_sent_msat || 0) / 1000);
transactionsReport[monthNumber].series[0].extra.total = transactionsReport[monthNumber].series[0].extra.total + 1; transactionsReport[monthNumber].series[0].extra.total = transactionsReport[monthNumber].series[0].extra.total + 1;
return this.transactionsReportSummary; return this.transactionsReportSummary;
}); });
filteredInvoices?.map((invoice) => { filteredInvoices?.map((invoice) => {
const monthNumber = new Date(+(invoice.paid_at || 0) * 1000).getMonth(); const monthNumber = new Date(+(invoice.paid_at || 0) * 1000).getMonth();
this.transactionsReportSummary.amountReceivedSelectedPeriod = this.transactionsReportSummary.amountReceivedSelectedPeriod + (invoice.msatoshi_received || 0); this.transactionsReportSummary.amountReceivedSelectedPeriod = this.transactionsReportSummary.amountReceivedSelectedPeriod + (invoice.amount_received_msat || 0);
transactionsReport[monthNumber].series[1].value = transactionsReport[monthNumber].series[1].value + ((invoice.msatoshi_received || 0) / 1000); transactionsReport[monthNumber].series[1].value = transactionsReport[monthNumber].series[1].value + ((invoice.amount_received_msat || 0) / 1000);
transactionsReport[monthNumber].series[1].extra.total = transactionsReport[monthNumber].series[1].extra.total + 1; transactionsReport[monthNumber].series[1].extra.total = transactionsReport[monthNumber].series[1].extra.total + 1;
return this.transactionsReportSummary; return this.transactionsReportSummary;
}); });
@ -140,15 +140,15 @@ export class CLNTransactionsReportComponent implements OnInit, OnDestroy {
} }
filteredPayments?.map((payment) => { filteredPayments?.map((payment) => {
const dateNumber = Math.floor((+(payment.created_at || 0) - startDateInSeconds) / this.secondsInADay); const dateNumber = Math.floor((+(payment.created_at || 0) - startDateInSeconds) / this.secondsInADay);
this.transactionsReportSummary.amountPaidSelectedPeriod = this.transactionsReportSummary.amountPaidSelectedPeriod + (payment.msatoshi_sent || 0); this.transactionsReportSummary.amountPaidSelectedPeriod = this.transactionsReportSummary.amountPaidSelectedPeriod + (payment.amount_sent_msat || 0);
transactionsReport[dateNumber].series[0].value = transactionsReport[dateNumber].series[0].value + ((payment.msatoshi_sent || 0) / 1000); transactionsReport[dateNumber].series[0].value = transactionsReport[dateNumber].series[0].value + ((payment.amount_sent_msat || 0) / 1000);
transactionsReport[dateNumber].series[0].extra.total = transactionsReport[dateNumber].series[0].extra.total + 1; transactionsReport[dateNumber].series[0].extra.total = transactionsReport[dateNumber].series[0].extra.total + 1;
return this.transactionsReportSummary; return this.transactionsReportSummary;
}); });
filteredInvoices?.map((invoice) => { filteredInvoices?.map((invoice) => {
const dateNumber = Math.floor((+(invoice.paid_at || 0) - startDateInSeconds) / this.secondsInADay); const dateNumber = Math.floor((+(invoice.paid_at || 0) - startDateInSeconds) / this.secondsInADay);
this.transactionsReportSummary.amountReceivedSelectedPeriod = this.transactionsReportSummary.amountReceivedSelectedPeriod + (invoice.msatoshi_received || 0); this.transactionsReportSummary.amountReceivedSelectedPeriod = this.transactionsReportSummary.amountReceivedSelectedPeriod + (invoice.amount_received_msat || 0);
transactionsReport[dateNumber].series[1].value = transactionsReport[dateNumber].series[1].value + ((invoice.msatoshi_received || 0) / 1000); transactionsReport[dateNumber].series[1].value = transactionsReport[dateNumber].series[1].value + ((invoice.amount_received_msat || 0) / 1000);
transactionsReport[dateNumber].series[1].extra.total = transactionsReport[dateNumber].series[1].extra.total + 1; transactionsReport[dateNumber].series[1].extra.total = transactionsReport[dateNumber].series[1].extra.total + 1;
return this.transactionsReportSummary; return this.transactionsReportSummary;
}); });

@ -58,15 +58,30 @@
</ng-container> </ng-container>
<ng-container matColumnDef="in_msatoshi"> <ng-container matColumnDef="in_msatoshi">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount In (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount In (Sats)</th>
<td *matCellDef="let fhEvent" mat-cell><span fxLayoutAlign="end center">{{fhEvent?.in_msatoshi/1000 | number:fhEvent?.in_msatoshi < 1000 ? '1.0-4' : '1.0-0'}}</span></td> <td *matCellDef="let fhEvent" mat-cell>
<span fxLayoutAlign="end center">
{{fhEvent?.in_msat/1000 | number:fhEvent?.in_msat < 1000 ? '1.0-4' : '1.0-0'}}
</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="out_msatoshi"> <ng-container matColumnDef="out_msatoshi">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount Out (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount Out (Sats)</th>
<td *matCellDef="let fhEvent" mat-cell><span fxLayoutAlign="end center">{{fhEvent?.out_msatoshi/1000 | number:fhEvent?.out_msatoshi < 1000 ? '1.0-4' : '1.0-0'}}</span></td> <td *matCellDef="let fhEvent" mat-cell>
<span fxLayoutAlign="end center">
{{fhEvent?.out_msat/1000 | number:fhEvent?.out_msat < 1000 ? '1.0-4' : '1.0-0'}}
</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="fee"> <ng-container matColumnDef="fee">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Fee (mSats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Fee (mSat)</th>
<td *matCellDef="let fhEvent" mat-cell><span fxLayoutAlign="end center">{{fhEvent?.fee | number:'1.0-0'}}</span></td> <td *matCellDef="let fhEvent" mat-cell>
<span *ngIf="fhEvent?.fee" fxLayoutAlign="end center">
{{fhEvent?.fee | number:'1.0-0'}}
</span>
<span *ngIf="!fhEvent?.fee && fhEvent?.fee_msat" fxLayoutAlign="end center">
{{fhEvent?.fee_msat | number:'1.0-0'}}
</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th *matHeaderCellDef mat-header-cell> <th *matHeaderCellDef mat-header-cell>

@ -1,5 +1,4 @@
import { Component, OnInit, ViewChild, OnDestroy, AfterViewInit } from '@angular/core'; import { Component, OnInit, ViewChild, OnDestroy, AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -56,13 +55,11 @@ export class CLNFailedTransactionsComponent implements OnInit, AfterViewInit, On
public apiCallStatusEnum = APICallStatusEnum; public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private datePipe: DatePipe, private router: Router, private camelCaseWithReplace: CamelCaseWithReplacePipe) { constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private datePipe: DatePipe, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.screenSize = this.commonService.getScreenSize(); this.screenSize = this.commonService.getScreenSize();
} }
ngOnInit() { ngOnInit() {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.store.dispatch(getForwardingHistory({ payload: { status: CLNForwardingEventsStatusEnum.FAILED } })); this.store.dispatch(getForwardingHistory({ payload: { status: CLNForwardingEventsStatusEnum.FAILED } }));
this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])). this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])).
subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => {
@ -110,9 +107,9 @@ export class CLNFailedTransactionsComponent implements OnInit, AfterViewInit, On
{ key: 'resolved_time', value: selFEvent.resolved_time, title: 'Resolved Time', width: 50, type: DataTypeEnum.DATE_TIME }], { key: 'resolved_time', value: selFEvent.resolved_time, title: 'Resolved Time', width: 50, type: DataTypeEnum.DATE_TIME }],
[{ key: 'in_channel_alias', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING }, [{ key: 'in_channel_alias', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING },
{ key: 'out_channel_alias', value: selFEvent.out_channel_alias, title: 'Outbound Channel', width: 50, type: DataTypeEnum.STRING }], { key: 'out_channel_alias', value: selFEvent.out_channel_alias, title: 'Outbound Channel', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'in_msatoshi', value: selFEvent.in_msatoshi, title: 'Amount In (mSats)', width: 33, type: DataTypeEnum.NUMBER }, [{ key: 'in_msatoshi', value: selFEvent.in_msat, title: 'Amount In (mSats)', width: 33, type: DataTypeEnum.NUMBER },
{ key: 'out_msatoshi', value: selFEvent.out_msatoshi, title: 'Amount Out (mSats)', width: 33, type: DataTypeEnum.NUMBER }, { key: 'out_msatoshi', value: selFEvent.out_msat, title: 'Amount Out (mSats)', width: 33, type: DataTypeEnum.NUMBER },
{ key: 'fee', value: selFEvent.fee, title: 'Fee (mSats)', width: 34, type: DataTypeEnum.NUMBER }] { key: 'fee', value: selFEvent.fee_msat, title: 'Fee (mSats)', width: 34, type: DataTypeEnum.NUMBER }]
]; ];
if (selFEvent.payment_hash) { if (selFEvent.payment_hash) {
reorderedFHEvent?.unshift([{ key: 'payment_hash', value: selFEvent.payment_hash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING }]); reorderedFHEvent?.unshift([{ key: 'payment_hash', value: selFEvent.payment_hash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING }]);
@ -142,12 +139,11 @@ export class CLNFailedTransactionsComponent implements OnInit, AfterViewInit, On
let rowToFilter = ''; let rowToFilter = '';
switch (this.selFilterBy) { switch (this.selFilterBy) {
case 'all': case 'all':
rowToFilter = (rowData.received_time ? this.datePipe.transform(new Date(rowData.received_time * 1000), 'dd/MMM/YYYY HH:mm')!.toLowerCase() : '') + rowToFilter = (rowData.received_time ? this.datePipe.transform(new Date(rowData.received_time * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() + ' ' : '') +
(rowData.resolved_time ? this.datePipe.transform(new Date(rowData.resolved_time * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() : '') + (rowData.resolved_time ? this.datePipe.transform(new Date(rowData.resolved_time * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() + ' ' : '') +
(rowData.payment_hash ? rowData.payment_hash.toLowerCase() : '') + (rowData.in_channel ? rowData.in_channel.toLowerCase() + ' ' : '') + (rowData.out_channel ? rowData.out_channel.toLowerCase() + ' ' : '') +
(rowData.in_channel ? rowData.in_channel.toLowerCase() : '') + (rowData.out_channel ? rowData.out_channel.toLowerCase() : '') + (rowData.in_channel_alias ? rowData.in_channel_alias.toLowerCase() + ' ' : '') + (rowData.out_channel_alias ? rowData.out_channel_alias.toLowerCase() + ' ' : '') +
(rowData.in_channel_alias ? rowData.in_channel_alias.toLowerCase() : '') + (rowData.out_channel_alias ? rowData.out_channel_alias.toLowerCase() : '') + (rowData.fee_msat ? rowData.fee_msat + ' ' : '') + (rowData.in_msat ? (+rowData.in_msat / 1000) + ' ' : '') + (rowData.out_msat ? (+rowData.out_msat / 1000) + ' ' : '') + (rowData.fee_msat ? rowData.fee_msat + ' ' : '');
(rowData.in_msatoshi ? (rowData.in_msatoshi / 1000) : '') + (rowData.out_msatoshi ? (rowData.out_msatoshi / 1000) : '') + (rowData.fee ? rowData.fee : '');
break; break;
case 'received_time': case 'received_time':
@ -155,9 +151,16 @@ export class CLNFailedTransactionsComponent implements OnInit, AfterViewInit, On
rowToFilter = this.datePipe.transform(new Date((rowData[this.selFilterBy] || 0) * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() || ''; rowToFilter = this.datePipe.transform(new Date((rowData[this.selFilterBy] || 0) * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() || '';
break; break;
case 'fee':
rowToFilter = ((rowData['fee_msat'] || 0))?.toString() || '';
break;
case 'in_msatoshi': case 'in_msatoshi':
rowToFilter = (+(rowData['in_msat'] || 0) / 1000)?.toString() || '';
break;
case 'out_msatoshi': case 'out_msatoshi':
rowToFilter = ((+(rowData[this.selFilterBy] || 0)) / 1000)?.toString() || ''; rowToFilter = (+(rowData['out_msat'] || 0) / 1000)?.toString() || '';
break; break;
default: default:
@ -171,7 +174,21 @@ export class CLNFailedTransactionsComponent implements OnInit, AfterViewInit, On
loadFailedEventsTable(forwardingEvents: ForwardingEvent[]) { loadFailedEventsTable(forwardingEvents: ForwardingEvent[]) {
this.failedForwardingEvents = new MatTableDataSource<ForwardingEvent>([...forwardingEvents]); this.failedForwardingEvents = new MatTableDataSource<ForwardingEvent>([...forwardingEvents]);
this.failedForwardingEvents.sort = this.sort; this.failedForwardingEvents.sort = this.sort;
this.failedForwardingEvents.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null); this.failedForwardingEvents.sortingDataAccessor = (data: any, sortHeaderId: string) => {
switch (sortHeaderId) {
case 'in_msatoshi':
return data['in_msat'];
case 'out_msatoshi':
return data['out_msat'];
case 'fee':
return data['fee_msat'];
default:
return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null;
}
};
this.failedForwardingEvents.paginator = this.paginator; this.failedForwardingEvents.paginator = this.paginator;
this.setFilterPredicate(); this.setFilterPredicate();
this.applyFilter(); this.applyFilter();

@ -60,15 +60,30 @@
</ng-container> </ng-container>
<ng-container matColumnDef="in_msatoshi"> <ng-container matColumnDef="in_msatoshi">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount In (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount In (Sats)</th>
<td *matCellDef="let fhEvent" mat-cell><span fxLayoutAlign="end center">{{fhEvent?.in_msatoshi/1000 | number:fhEvent?.in_msatoshi < 1000 ? '1.0-4' : '1.0-0'}}</span></td> <td *matCellDef="let fhEvent" mat-cell>
<span fxLayoutAlign="end center">
{{fhEvent?.in_msat/1000 | number:fhEvent?.in_msat < 1000 ? '1.0-4' : '1.0-0'}}
</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="out_msatoshi"> <ng-container matColumnDef="out_msatoshi">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount Out (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount Out (Sats)</th>
<td *matCellDef="let fhEvent" mat-cell><span fxLayoutAlign="end center">{{fhEvent?.out_msatoshi/1000 | number:fhEvent?.out_msatoshi < 1000 ? '1.0-4' : '1.0-0'}}</span></td> <td *matCellDef="let fhEvent" mat-cell>
<span fxLayoutAlign="end center">
{{fhEvent?.out_msat/1000 | number:fhEvent?.out_msat < 1000 ? '1.0-4' : '1.0-0'}}
</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="fee"> <ng-container matColumnDef="fee">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Fee (mSat)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Fee (mSat)</th>
<td *matCellDef="let fhEvent" mat-cell><span fxLayoutAlign="end center">{{fhEvent?.fee | number}}</span></td> <td *matCellDef="let fhEvent" mat-cell>
<span *ngIf="fhEvent?.fee" fxLayoutAlign="end center">
{{fhEvent?.fee | number}}
</span>
<span *ngIf="!fhEvent?.fee && fhEvent?.fee_msat" fxLayoutAlign="end center">
{{fhEvent?.fee_msat | number}}
</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th *matHeaderCellDef mat-header-cell> <th *matHeaderCellDef mat-header-cell>

@ -1,5 +1,4 @@
import { Component, OnInit, OnChanges, ViewChild, OnDestroy, SimpleChanges, Input, AfterViewInit } from '@angular/core'; import { Component, OnInit, OnChanges, ViewChild, OnDestroy, SimpleChanges, Input, AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil, take } from 'rxjs/operators'; import { takeUntil, take } from 'rxjs/operators';
@ -56,13 +55,11 @@ export class CLNForwardingHistoryComponent implements OnInit, OnChanges, AfterVi
public apiCallStatusEnum = APICallStatusEnum; public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private datePipe: DatePipe, private router: Router, private camelCaseWithReplace: CamelCaseWithReplacePipe) { constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private datePipe: DatePipe, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.screenSize = this.commonService.getScreenSize(); this.screenSize = this.commonService.getScreenSize();
} }
ngOnInit() { ngOnInit() {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])). this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])).
subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = ''; this.errorMessage = '';
@ -132,16 +129,18 @@ export class CLNForwardingHistoryComponent implements OnInit, OnChanges, AfterVi
onForwardingEventClick(selFEvent: ForwardingEvent, event: any) { onForwardingEventClick(selFEvent: ForwardingEvent, event: any) {
const reorderedFHEvent = [ const reorderedFHEvent = [
[{ key: 'payment_hash', value: selFEvent.payment_hash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING }],
[{ key: 'status', value: 'Settled', title: 'Status', width: 50, type: DataTypeEnum.STRING }, [{ key: 'status', value: 'Settled', title: 'Status', width: 50, type: DataTypeEnum.STRING },
{ key: 'fee', value: selFEvent.fee, title: 'Fee (mSats)', width: 50, type: DataTypeEnum.NUMBER }], { key: 'fee', value: selFEvent.fee_msat, title: 'Fee (mSats)', width: 50, type: DataTypeEnum.NUMBER }],
[{ key: 'received_time', value: selFEvent.received_time, title: 'Received Time', width: 50, type: DataTypeEnum.DATE_TIME }, [{ key: 'received_time', value: selFEvent.received_time, title: 'Received Time', width: 50, type: DataTypeEnum.DATE_TIME },
{ key: 'resolved_time', value: selFEvent.resolved_time, title: 'Resolved Time', width: 50, type: DataTypeEnum.DATE_TIME }], { key: 'resolved_time', value: selFEvent.resolved_time, title: 'Resolved Time', width: 50, type: DataTypeEnum.DATE_TIME }],
[{ key: 'in_channel', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING }, [{ key: 'in_channel', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING },
{ key: 'out_channel', value: selFEvent.out_channel_alias, title: 'Outbound Channel', width: 50, type: DataTypeEnum.STRING }], { key: 'out_channel', value: selFEvent.out_channel_alias, title: 'Outbound Channel', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'in_msatoshi', value: selFEvent.in_msatoshi, title: 'In (mSats)', width: 50, type: DataTypeEnum.NUMBER }, [{ key: 'in_msatoshi', value: selFEvent.in_msat, title: 'In (mSats)', width: 50, type: DataTypeEnum.NUMBER },
{ key: 'out_msatoshi', value: selFEvent.out_msatoshi, title: 'Out (mSats)', width: 50, type: DataTypeEnum.NUMBER }] { key: 'out_msatoshi', value: selFEvent.out_msat, title: 'Out (mSats)', width: 50, type: DataTypeEnum.NUMBER }]
]; ];
if (selFEvent.payment_hash) {
reorderedFHEvent.unshift([{ key: 'payment_hash', value: selFEvent.payment_hash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING }]);
}
this.store.dispatch(openAlert({ this.store.dispatch(openAlert({
payload: { payload: {
data: { data: {
@ -173,7 +172,7 @@ export class CLNForwardingHistoryComponent implements OnInit, OnChanges, AfterVi
(rowData.resolved_time ? this.datePipe.transform(new Date(rowData.resolved_time * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() + ' ' : '') + (rowData.resolved_time ? this.datePipe.transform(new Date(rowData.resolved_time * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() + ' ' : '') +
(rowData.in_channel ? rowData.in_channel.toLowerCase() + ' ' : '') + (rowData.out_channel ? rowData.out_channel.toLowerCase() + ' ' : '') + (rowData.in_channel ? rowData.in_channel.toLowerCase() + ' ' : '') + (rowData.out_channel ? rowData.out_channel.toLowerCase() + ' ' : '') +
(rowData.in_channel_alias ? rowData.in_channel_alias.toLowerCase() + ' ' : '') + (rowData.out_channel_alias ? rowData.out_channel_alias.toLowerCase() + ' ' : '') + (rowData.in_channel_alias ? rowData.in_channel_alias.toLowerCase() + ' ' : '') + (rowData.out_channel_alias ? rowData.out_channel_alias.toLowerCase() + ' ' : '') +
(rowData.in_msatoshi ? (rowData.in_msatoshi / 1000) + ' ' : '') + (rowData.out_msatoshi ? (rowData.out_msatoshi / 1000) + ' ' : '') + (rowData.fee ? rowData.fee + ' ' : ''); (rowData.in_msat ? (+rowData.in_msat / 1000) + ' ' : '') + (rowData.out_msat ? (+rowData.out_msat / 1000) + ' ' : '') + (rowData.fee_msat ? rowData.fee_msat + ' ' : '');
break; break;
case 'received_time': case 'received_time':
@ -181,9 +180,16 @@ export class CLNForwardingHistoryComponent implements OnInit, OnChanges, AfterVi
rowToFilter = this.datePipe.transform(new Date((rowData[this.selFilterBy] || 0) * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() || ''; rowToFilter = this.datePipe.transform(new Date((rowData[this.selFilterBy] || 0) * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() || '';
break; break;
case 'fee':
rowToFilter = ((+(rowData['fee_msat'] || 0)))?.toString() || '';
break;
case 'in_msatoshi': case 'in_msatoshi':
rowToFilter = ((+(rowData['in_msat'] || 0)) / 1000)?.toString() || '';
break;
case 'out_msatoshi': case 'out_msatoshi':
rowToFilter = ((+(rowData[this.selFilterBy] || 0)) / 1000)?.toString() || ''; rowToFilter = ((+(rowData['out_msat'] || 0)) / 1000)?.toString() || '';
break; break;
default: default:
@ -197,7 +203,21 @@ export class CLNForwardingHistoryComponent implements OnInit, OnChanges, AfterVi
loadForwardingEventsTable(forwardingEvents: ForwardingEvent[]) { loadForwardingEventsTable(forwardingEvents: ForwardingEvent[]) {
this.forwardingHistoryEvents = new MatTableDataSource<ForwardingEvent>([...forwardingEvents]); this.forwardingHistoryEvents = new MatTableDataSource<ForwardingEvent>([...forwardingEvents]);
this.forwardingHistoryEvents.sort = this.sort; this.forwardingHistoryEvents.sort = this.sort;
this.forwardingHistoryEvents.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null); this.forwardingHistoryEvents.sortingDataAccessor = (data: any, sortHeaderId: string) => {
switch (sortHeaderId) {
case 'in_msatoshi':
return data['in_msat'];
case 'out_msatoshi':
return data['out_msat'];
case 'fee':
return data['fee_msat'];
default:
return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null;
}
};
this.forwardingHistoryEvents.paginator = this.paginator; this.forwardingHistoryEvents.paginator = this.paginator;
this.setFilterPredicate(); this.setFilterPredicate();
this.applyFilter(); this.applyFilter();

@ -54,7 +54,11 @@
</ng-container> </ng-container>
<ng-container matColumnDef="in_msatoshi"> <ng-container matColumnDef="in_msatoshi">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount In (Sats)</th> <th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">Amount In (Sats)</th>
<td *matCellDef="let fhEvent" mat-cell><span fxLayoutAlign="end center">{{fhEvent?.in_msatoshi/1000 | number:fhEvent?.in_msatoshi < 1000 ? '1.0-4' : '1.0-0'}}</span></td> <td *matCellDef="let fhEvent" mat-cell>
<span fxLayoutAlign="end center">
{{fhEvent?.in_msat/1000 | number:fhEvent?.in_msat < 1000 ? '1.0-4' : '1.0-0'}}
</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="style"> <ng-container matColumnDef="style">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Style</th> <th *matHeaderCellDef mat-header-cell mat-sort-header>Style</th>

@ -1,5 +1,4 @@
import { Component, OnInit, ViewChild, OnDestroy, AfterViewInit } from '@angular/core'; import { Component, OnInit, ViewChild, OnDestroy, AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -57,13 +56,11 @@ export class CLNLocalFailedTransactionsComponent implements OnInit, AfterViewIni
public apiCallStatusEnum = APICallStatusEnum; public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private datePipe: DatePipe, private router: Router, private camelCaseWithReplace: CamelCaseWithReplacePipe) { constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private datePipe: DatePipe, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.screenSize = this.commonService.getScreenSize(); this.screenSize = this.commonService.getScreenSize();
} }
ngOnInit() { ngOnInit() {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.store.dispatch(getForwardingHistory({ payload: { status: CLNForwardingEventsStatusEnum.LOCAL_FAILED } })); this.store.dispatch(getForwardingHistory({ payload: { status: CLNForwardingEventsStatusEnum.LOCAL_FAILED } }));
this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])). this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])).
subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => {
@ -109,7 +106,7 @@ export class CLNLocalFailedTransactionsComponent implements OnInit, AfterViewIni
const reorderedFHEvent = [ const reorderedFHEvent = [
[{ key: 'received_time', value: selFEvent.received_time, title: 'Received Time', width: 50, type: DataTypeEnum.DATE_TIME }, [{ key: 'received_time', value: selFEvent.received_time, title: 'Received Time', width: 50, type: DataTypeEnum.DATE_TIME },
{ key: 'in_channel_alias', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING }], { key: 'in_channel_alias', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'in_msatoshi', value: selFEvent.in_msatoshi, title: 'Amount In (mSats)', width: 100, type: DataTypeEnum.NUMBER }], [{ key: 'in_msatoshi', value: selFEvent.in_msat, title: 'Amount In (mSats)', width: 100, type: DataTypeEnum.NUMBER }],
[{ key: 'failreason', value: selFEvent.failreason ? this.CLNFailReason[selFEvent.failreason] : '', title: 'Reason for Failure', width: 100, type: DataTypeEnum.STRING }] [{ key: 'failreason', value: selFEvent.failreason ? this.CLNFailReason[selFEvent.failreason] : '', title: 'Reason for Failure', width: 100, type: DataTypeEnum.STRING }]
]; ];
this.store.dispatch(openAlert({ this.store.dispatch(openAlert({
@ -140,7 +137,7 @@ export class CLNLocalFailedTransactionsComponent implements OnInit, AfterViewIni
rowToFilter = (rowData.received_time ? this.datePipe.transform(new Date(rowData.received_time * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() : '') + rowToFilter = (rowData.received_time ? this.datePipe.transform(new Date(rowData.received_time * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() : '') +
(rowData.in_channel_alias ? rowData.in_channel_alias.toLowerCase() : '') + (rowData.in_channel_alias ? rowData.in_channel_alias.toLowerCase() : '') +
((rowData.failreason && this.CLNFailReason[rowData.failreason]) ? this.CLNFailReason[rowData.failreason].toLowerCase() : '') + ((rowData.failreason && this.CLNFailReason[rowData.failreason]) ? this.CLNFailReason[rowData.failreason].toLowerCase() : '') +
(rowData.in_msatoshi ? (rowData.in_msatoshi / 1000) : ''); (rowData.in_msat ? rowData.in_msat : '');
break; break;
case 'received_time': case 'received_time':
@ -148,7 +145,7 @@ export class CLNLocalFailedTransactionsComponent implements OnInit, AfterViewIni
break; break;
case 'in_msatoshi': case 'in_msatoshi':
rowToFilter = ((+(rowData.in_msatoshi || 0)) / 1000)?.toString() || ''; rowToFilter = ((+(rowData['in_msat'] || 0)) / 1000)?.toString() || '';
break; break;
case 'failreason': case 'failreason':
@ -168,6 +165,9 @@ export class CLNLocalFailedTransactionsComponent implements OnInit, AfterViewIni
this.failedLocalForwardingEvents.sort = this.sort; this.failedLocalForwardingEvents.sort = this.sort;
this.failedLocalForwardingEvents.sortingDataAccessor = (data: LocalFailedEvent, sortHeaderId: string) => { this.failedLocalForwardingEvents.sortingDataAccessor = (data: LocalFailedEvent, sortHeaderId: string) => {
switch (sortHeaderId) { switch (sortHeaderId) {
case 'in_msatoshi':
return data['in_msat'];
case 'failreason': case 'failreason':
return data.failreason ? this.CLNFailReason[data.failreason] : ''; return data.failreason ? this.CLNFailReason[data.failreason] : '';

@ -5,16 +5,6 @@
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="space-between center" fxLayoutAlign="start stretch" class="page-sub-title-container w-100" [ngClass]="{'mt-2': screenSize === screenSizeEnum.XS, 'mt-1': screenSize === screenSizeEnum.SM}"> <div fxLayout="column" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="space-between center" fxLayoutAlign="start stretch" class="page-sub-title-container w-100" [ngClass]="{'mt-2': screenSize === screenSizeEnum.XS, 'mt-1': screenSize === screenSizeEnum.SM}">
<div fxFlex="70">Incoming</div> <div fxFlex="70">Incoming</div>
<div fxFlex.gt-xs="30" fxLayoutAlign.gt-xs="space-between center" fxLayout="row" fxLayoutAlign="space-between stretch"> <div fxFlex.gt-xs="30" fxLayoutAlign.gt-xs="space-between center" fxLayout="row" fxLayoutAlign="space-between stretch">
<!-- <mat-form-field fxLayout="column" fxFlex="49">
<mat-label>Filter By</mat-label>
<mat-select tabindex="1" [(ngModel)]="selFilterByIn" (selectionChange)="selFilterIn=''; applyIncomingFilter()" name="filterByIn">
<perfect-scrollbar><mat-option *ngFor="let column of ['all'].concat(displayedColumns.slice(0, -1))" [value]="column">{{getLabel(column)}}</mat-option></perfect-scrollbar>
</mat-select>
</mat-form-field>
<mat-form-field fxLayout="column" fxFlex="49">
<mat-label>Filter</mat-label>
<input matInput [(ngModel)]="selFilterIn" (input)="applyIncomingFilter()" (keyup)="applyIncomingFilter()" name="filterin">
</mat-form-field> -->
</div> </div>
</div> </div>
<div fxLayout="column" fxLayoutAlign="start stretch" fxFlex="100" class="table-container" [perfectScrollbar]> <div fxLayout="column" fxLayoutAlign="start stretch" fxFlex="100" class="table-container" [perfectScrollbar]>

@ -132,29 +132,6 @@ export class CLNRoutingPeersComponent implements OnInit, OnChanges, AfterViewIni
setFilterPredicate() { setFilterPredicate() {
this.routingPeersIncoming.filterPredicate = (rpIn: RoutingPeer, fltr: string) => JSON.stringify(rpIn).toLowerCase().includes(fltr); this.routingPeersIncoming.filterPredicate = (rpIn: RoutingPeer, fltr: string) => JSON.stringify(rpIn).toLowerCase().includes(fltr);
this.routingPeersOutgoing.filterPredicate = (rpOut: RoutingPeer, fltr: string) => JSON.stringify(rpOut).toLowerCase().includes(fltr); this.routingPeersOutgoing.filterPredicate = (rpOut: RoutingPeer, fltr: string) => JSON.stringify(rpOut).toLowerCase().includes(fltr);
// this.routingPeersIncoming.filterPredicate = (rowData: RoutingPeer, fltr: string) => {
// let rowToFilter = '';
// switch (this.selFilterBy) {
// case 'all':
// for (let i = 0; i < this.displayedColumns.length - 1; i++) {
// rowToFilter = rowToFilter + (
// (this.displayedColumns[i] === '') ?
// (rowData ? rowData..toLowerCase() : '') :
// (rowData[this.displayedColumns[i]] ? rowData[this.displayedColumns[i]].toLowerCase() : '')
// ) + ', ';
// }
// break;
// case '':
// rowToFilter = (rowData ? rowData..toLowerCase() : '');
// break;
// default:
// rowToFilter = (rowData[this.selFilterBy] ? rowData[this.selFilterBy].toLowerCase() : '');
// break;
// }
// return rowToFilter.includes(fltr);
// };
} }
loadRoutingPeersTable(events: ForwardingEvent[]) { loadRoutingPeersTable(events: ForwardingEvent[]) {
@ -187,18 +164,18 @@ export class CLNRoutingPeersComponent implements OnInit, OnChanges, AfterViewIni
const incoming: any = incomingResults?.find((result) => result.channel_id === event.in_channel); const incoming: any = incomingResults?.find((result) => result.channel_id === event.in_channel);
const outgoing: any = outgoingResults?.find((result) => result.channel_id === event.out_channel); const outgoing: any = outgoingResults?.find((result) => result.channel_id === event.out_channel);
if (!incoming) { if (!incoming) {
incomingResults.push({ channel_id: event.in_channel, alias: event.in_channel_alias, events: 1, total_amount: event.in_msatoshi, total_fee: ((event.in_msatoshi || 0) - (event.out_msatoshi || 0)) }); incomingResults.push({ channel_id: event.in_channel, alias: event.in_channel_alias, events: 1, total_amount: (+(event.in_msat || 0)), total_fee: ((+(event.in_msat || 0)) - (+(event.out_msat || 0))) });
} else { } else {
incoming.events++; incoming.events++;
incoming.total_amount = +incoming.total_amount + +(event.in_msatoshi || 0); incoming.total_amount = +incoming.total_amount + +(event.in_msat || 0);
incoming.total_fee = +incoming.total_fee + ((event.in_msatoshi || 0) - (event.out_msatoshi || 0)); incoming.total_fee = +incoming.total_fee + ((+(event.in_msat || 0)) - (+(event.out_msat || 0)));
} }
if (!outgoing) { if (!outgoing) {
outgoingResults.push({ channel_id: event.out_channel, alias: event.out_channel_alias, events: 1, total_amount: event.out_msatoshi, total_fee: ((event.in_msatoshi || 0) - (event.out_msatoshi || 0)) }); outgoingResults.push({ channel_id: event.out_channel, alias: event.out_channel_alias, events: 1, total_amount: (+(event.out_msat || 0)), total_fee: ((+(event.in_msat || 0)) - (+(event.out_msat || 0))) });
} else { } else {
outgoing.events++; outgoing.events++;
outgoing.total_amount = +outgoing.total_amount + +(event.out_msatoshi || 0); outgoing.total_amount = +outgoing.total_amount + +(event.out_msat || 0);
outgoing.total_fee = +outgoing.total_fee + ((event.in_msatoshi || 0) - (event.out_msatoshi || 0)); outgoing.total_fee = +outgoing.total_fee + ((+(event.in_msat || 0)) - (+(event.out_msat || 0)));
} }
}); });
return [this.commonService.sortDescByKey(incomingResults, 'total_fee'), this.commonService.sortDescByKey(outgoingResults, 'total_fee')]; return [this.commonService.sortDescByKey(incomingResults, 'total_fee'), this.commonService.sortDescByKey(outgoingResults, 'total_fee')];

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

Loading…
Cancel
Save