Merge pull request #1127 from Ride-The-Lightning/page-layout

Page layout
pull/1128/head
ShahanaFarooqui 2 years ago committed by GitHub
commit fc459774ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -48,7 +48,7 @@
"curly": "error",
"no-unused-expressions": "error",
"strict": "error",
"max-len": ["error", { "code": 450 }],
"max-len": ["error", { "code": 320 }],
"no-multiple-empty-lines": "error",
"no-trailing-spaces": "error",
"quote-props": ["error", "as-needed"],

@ -62,7 +62,7 @@ export const getInfo = (req, res, next) => {
req.session.selectedNode.ln_version = body.version || '';
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Core Lightning\'s Websocket Server.' });
clWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
databaseService.loadDatabase(req.session);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(body);
}

@ -31,10 +31,6 @@ export const listInvoices = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List URL', data: options.url });
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: body });
if (body.invoices && body.invoices.length > 0) {
body.invoices = common.sortDescByKey(body.invoices, 'expires_at');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Sorted Invoices List Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'List Invoices Error', req.session.selectedNode);

@ -11,9 +11,6 @@ export const listOfferBookmarks = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offer Bookmarks..' });
databaseService.find(req.session.selectedNode, CollectionsEnum.OFFERS).then((offers) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmarks Received', data: offers });
if (offers && offers.length > 0) {
offers = common.sortDescByKey(offers, 'lastUpdatedAt');
}
res.status(200).json(offers);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'Offer Bookmarks Error', req.session.selectedNode);
@ -22,7 +19,7 @@ export const listOfferBookmarks = (req, res, next) => {
};
export const deleteOfferBookmark = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Deleting Offer Bookmark..' });
databaseService.destroy(req.session.selectedNode, CollectionsEnum.OFFERS, CollectionFieldsEnum.BOLT12, req.params.offerStr).then((deleteRes) => {
databaseService.remove(req.session.selectedNode, CollectionsEnum.OFFERS, CollectionFieldsEnum.BOLT12, req.params.offerStr).then((deleteRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmark Deleted', data: deleteRes });
res.status(204).json(req.params.offerStr);
}).catch((errRes) => {

@ -44,9 +44,6 @@ export const getUTXOs = (req, res, next) => {
}
options.url = req.session.selectedNode.ln_server_url + '/v1/listFunds';
request(options).then((body) => {
if (body.outputs) {
body.outputs = common.sortDescByStrKey(body.outputs, 'status');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Funds List Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {

@ -73,10 +73,6 @@ export const listPayments = (req, res, next) => {
options.url = req.session.selectedNode.ln_server_url + '/v1/pay/listPayments';
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: body.payments });
if (body && body.payments && body.payments.length > 0) {
body.payments = common.sortDescByKey(body.payments, 'created_at');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sorted Payments List Received', data: body.payments });
res.status(200).json(groupBy(body.payments));
}).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode);
@ -108,19 +104,25 @@ export const postPayment = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent', data: body });
if (req.body.paymentType === 'OFFER') {
if (req.body.saveToDB && req.body.bolt12) {
const offerToUpdate = { bolt12: req.body.bolt12, amountmSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() };
const offerToUpdate = { bolt12: req.body.bolt12, amountMSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() };
if (req.body.vendor) {
offerToUpdate['vendor'] = req.body.vendor;
}
if (req.body.description) {
offerToUpdate['description'] = req.body.description;
}
return databaseService.update(req.session.selectedNode, CollectionsEnum.OFFERS, offerToUpdate, CollectionFieldsEnum.BOLT12, req.body.bolt12).then((updatedOffer) => {
logger.log({ level: 'DEBUG', fileName: 'Offer', msg: 'Offer Updated', data: updatedOffer });
return res.status(201).json({ paymentResponse: body, saveToDBResponse: updatedOffer });
}).catch((errDB) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB update error', error: errDB });
return res.status(201).json({ paymentResponse: body, saveToDBError: errDB });
// eslint-disable-next-line arrow-body-style
return databaseService.validateDocument(CollectionsEnum.OFFERS, offerToUpdate).then((validated) => {
return databaseService.update(req.session.selectedNode, CollectionsEnum.OFFERS, offerToUpdate, CollectionFieldsEnum.BOLT12, req.body.bolt12).then((updatedOffer) => {
logger.log({ level: 'DEBUG', fileName: 'Payments', msg: 'Offer Updated', data: updatedOffer });
return res.status(201).json({ paymentResponse: body, saveToDBResponse: updatedOffer });
}).catch((errDB) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB update error', error: errDB });
return res.status(201).json({ paymentResponse: body, saveToDBError: errDB });
});
}).catch((errValidation) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB validation error', error: errValidation });
return res.status(201).json({ paymentResponse: body, saveToDBError: errValidation });
});
}
else {

@ -17,9 +17,8 @@ export const getPeers = (req, res, next) => {
peer.alias = peer.id.substring(0, 20);
}
});
const peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers with Alias Received', data: peers });
res.status(200).json(peers);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers with Alias Received', data: body });
res.status(200).json(body || []);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Peers', 'List Peers Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
@ -33,12 +32,11 @@ export const postPeer = (req, res, next) => {
}
options.url = req.session.selectedNode.ln_server_url + '/v1/peer/connect';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: body });
request.post(options).then((connectRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: connectRes });
options.url = req.session.selectedNode.ln_server_url + '/v1/peer/listPeers';
request(options).then((body) => {
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
peers = common.newestOnTop(peers, 'id', req.body.id);
request(options).then((listPeersRes) => {
const peers = listPeersRes ? common.newestOnTop(listPeersRes, 'id', req.body.id) : [];
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers });
res.status(201).json(peers);
}).catch((errRes) => {

@ -88,9 +88,6 @@ export const arrangePayments = (selNode, body) => {
relayedEle.amountOut = Math.round(relayedEle.amountOut / 1000);
}
});
payments.sent = common.sortDescByKey(payments.sent, 'firstPartTimestamp');
payments.received = common.sortDescByKey(payments.received, 'firstPartTimestamp');
payments.relayed = common.sortDescByKey(payments.relayed, 'timestamp');
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Fees', msg: 'Arranged Payments Received', data: payments });
return payments;
};

@ -38,7 +38,7 @@ export const getInfo = (req, res, next) => {
body.lnImplementation = 'Eclair';
req.session.selectedNode.ln_version = body.version.split('-')[0] || '';
eclWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
databaseService.loadDatabase(req.session);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(body);
}).catch((errRes) => {

@ -71,10 +71,7 @@ export const listInvoices = (req, res, next) => {
const invoices = (!body[0] || body[0].length <= 0) ? [] : body[0];
pendingInvoices = (!body[1] || body[1].length <= 0) ? [] : body[1];
return Promise.all(invoices === null || invoices === void 0 ? void 0 : invoices.map((invoice) => getReceivedPaymentInfo(req.session.selectedNode.ln_server_url, invoice))).
then((values) => {
body = common.sortDescByKey(invoices, 'expiresAt');
return res.status(200).json(invoices);
});
then((values) => res.status(200).json(invoices));
});
}
else {
@ -86,7 +83,6 @@ export const listInvoices = (req, res, next) => {
if (invoices && invoices.length > 0) {
return Promise.all(invoices === null || invoices === void 0 ? void 0 : invoices.map((invoice) => getReceivedPaymentInfo(req.session.selectedNode.ln_server_url, invoice))).
then((values) => {
body = common.sortDescByKey(invoices, 'expiresAt');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Sorted Invoices List Received', data: invoices });
return res.status(200).json(invoices);
}).

@ -66,9 +66,6 @@ export const getTransactions = (req, res, next) => {
};
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Getting On Chain Transactions Options', data: options.form });
request.post(options).then((body) => {
if (body && body.length > 0) {
body = common.sortDescByKey(body, 'timestamp');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'On Chain Transactions Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {

@ -37,7 +37,6 @@ export const getPeers = (req, res, next) => {
peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20);
return peer;
});
body = common.sortDescByStrKey(body, 'alias');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body });
res.status(200).json(body);
});
@ -90,8 +89,7 @@ export const connectPeer = (req, res, next) => {
peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20);
return peer;
});
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
peers = common.newestOnTop(peers, 'nodeId', req.query.nodeId ? req.query.nodeId : req.query.uri ? req.query.uri.substring(0, req.query.uri.indexOf('@')) : '');
const peers = common.newestOnTop(body || [], 'nodeId', req.query.nodeId ? req.query.nodeId : req.query.uri ? req.query.uri.substring(0, req.query.uri.indexOf('@')) : '');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers });
res.status(201).json(peers);
});

@ -38,7 +38,6 @@ export const getAllChannels = (req, res, next) => {
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
return getAliasForChannel(req.session.selectedNode, channel);
})).then((values) => {
body.channels = common.sortDescByKey(body.channels, 'balancedness');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sorted Channels List Received', data: body });
return res.status(200).json(body);
}).catch((errRes) => {
@ -73,11 +72,11 @@ export const getPendingChannels = (req, res, next) => {
if (body.pending_open_channels && body.pending_open_channels.length > 0) {
(_a = body.pending_open_channels) === null || _a === void 0 ? void 0 : _a.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
if (body.pending_closing_channels && body.pending_closing_channels.length > 0) {
(_b = body.pending_closing_channels) === null || _b === void 0 ? void 0 : _b.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
if (body.pending_force_closing_channels && body.pending_force_closing_channels.length > 0) {
(_c = body.pending_force_closing_channels) === null || _c === void 0 ? void 0 : _c.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
(_b = body.pending_force_closing_channels) === null || _b === void 0 ? void 0 : _b.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
if (body.pending_closing_channels && body.pending_closing_channels.length > 0) {
(_c = body.pending_closing_channels) === null || _c === void 0 ? void 0 : _c.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
if (body.waiting_close_channels && body.waiting_close_channels.length > 0) {
(_d = body.waiting_close_channels) === null || _d === void 0 ? void 0 : _d.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
@ -110,7 +109,6 @@ export const getClosedChannels = (req, res, next) => {
channel.close_type = (!channel.close_type) ? 'COOPERATIVE_CLOSE' : channel.close_type;
return getAliasForChannel(req.session.selectedNode, channel);
})).then((values) => {
body.channels = common.sortDescByKey(body.channels, 'close_height');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels List Received', data: body });
return res.status(200).json(body);
}).catch((errRes) => {

@ -45,7 +45,7 @@ export const getInfo = (req, res, next) => {
else {
req.session.selectedNode.ln_version = body.version.split('-')[0] || '';
lndWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
databaseService.loadDatabase(req.session);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(body);
}

@ -46,7 +46,6 @@ export const listInvoices = (req, res, next) => {
invoice.r_hash = invoice.r_hash ? Buffer.from(invoice.r_hash, 'base64').toString('hex') : '';
invoice.description_hash = invoice.description_hash ? Buffer.from(invoice.description_hash, 'base64').toString('hex') : null;
});
body.invoices = common.sortDescByKey(body.invoices, 'creation_date');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Sorted Invoices List Received', data: body });
res.status(200).json(body);

@ -58,10 +58,6 @@ export const getPayments = (req, res, next) => {
options.url = req.session.selectedNode.ln_server_url + '/v1/payments?max_payments=' + req.query.max_payments + '&index_offset=' + req.query.index_offset + '&reversed=' + req.query.reversed;
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: body });
if (body.payments && body.payments.length > 0) {
body.payments = common.sortDescByKey(body.payments, 'creation_date');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sorted Payments List Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode);

@ -26,10 +26,6 @@ export const getPeers = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers List Received', data: body });
const peers = !body.peers ? [] : body.peers;
return Promise.all(peers === null || peers === void 0 ? void 0 : peers.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers with Alias before Sort', data: body });
if (body.peers) {
body.peers = common.sortDescByStrKey(body.peers, 'alias');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body.peers });
res.status(200).json(body.peers);
});
@ -56,7 +52,6 @@ export const postPeer = (req, res, next) => {
const peers = (!body.peers) ? [] : body.peers;
return Promise.all(peers === null || peers === void 0 ? void 0 : peers.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => {
if (body.peers) {
body.peers = common.sortDescByStrKey(body.peers, 'alias');
body.peers = common.newestOnTop(body.peers, 'pub_key', req.body.pubkey);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: body });
}

@ -46,9 +46,6 @@ export const getAllForwardingEvents = (req, start, end, offset, caller, callback
}
if (!body.last_offset_index || body.last_offset_index < offset + num_max_events) {
responseData[caller].last_offset_index = body.last_offset_index ? body.last_offset_index : 0;
if (responseData[caller].forwarding_events) {
responseData[caller].forwarding_events = common.sortDescByKey(responseData[caller].forwarding_events, 'timestamp');
}
return callback(responseData[caller]);
}
else {

@ -13,10 +13,6 @@ export const getTransactions = (req, res, next) => {
options.url = req.session.selectedNode.ln_server_url + '/v1/transactions';
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Transactions', msg: 'Transactions List Received', data: body });
if (body.transactions && body.transactions.length > 0) {
body.transactions = common.sortDescByKey(body.transactions, 'time_stamp');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Transactions', msg: 'Sorted Transactions List Received', data: body.transactions });
res.status(200).json(body.transactions);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Transactions', 'List Transactions Error', req.session.selectedNode);

@ -19,7 +19,7 @@ export const updateSelectedNode = (req, res, next) => {
if (req.headers && req.headers.authorization && req.headers.authorization !== '') {
wsServer.updateLNWSClientDetails(req.session.id, +req.session.selectedNode.index, +req.params.prevNodeIndex);
if (req.params.prevNodeIndex !== -1) {
databaseService.unloadDatabase(req.params.prevNodeIndex);
databaseService.unloadDatabase(req.params.prevNodeIndex, req.session.id);
}
}
const responseVal = !req.session.selectedNode.ln_node ? '' : req.session.selectedNode.ln_node;

@ -124,7 +124,7 @@ export const resetPassword = (req, res, next) => {
export const logoutUser = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Logged out' });
if (req.session.selectedNode && req.session.selectedNode.index) {
databaseService.unloadDatabase(+req.session.selectedNode.index);
databaseService.unloadDatabase(+req.session.selectedNode.index, req.session.id);
}
req.session.destroy((err) => {
res.clearCookie('connect.sid');

@ -216,10 +216,6 @@ export const swaps = (req, res, next) => {
options.url = options.url + '/v1/loop/swaps';
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Swaps Received', data: body });
if (body.swaps && body.swaps.length > 0) {
body.swaps = common.sortDescByKey(body.swaps, 'initiation_time');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Sorted Loop Swaps List Received', data: body });
}
res.status(200).json(body.swaps);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Loop', 'List Swaps Error', req.session.selectedNode);

@ -0,0 +1,33 @@
import { Database } from '../../utils/database.js';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { CollectionsEnum } from '../../models/database.model.js';
const logger = Logger;
const common = Common;
const databaseService = Database;
export const getPageSettings = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Getting Page Settings..' });
databaseService.find(req.session.selectedNode, CollectionsEnum.PAGE_SETTINGS).then((settings) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Page Settings Received', data: settings });
res.status(200).json(settings);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Page Settings', 'Page Settings Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const savePageSettings = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Saving Page Settings..' });
// eslint-disable-next-line arrow-body-style
return Promise.all(req.body.map((page) => databaseService.validateDocument(CollectionsEnum.PAGE_SETTINGS, page))).then((values) => {
return databaseService.insert(req.session.selectedNode, CollectionsEnum.PAGE_SETTINGS, req.body).then((insertRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Page Settings Updated', data: insertRes });
res.status(201).json(insertRes);
}).catch((insertErrRes) => {
const err = common.handleError(insertErrRes, 'Page Settings', 'Page Settings Update Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Page Settings', 'Page Settings Validation Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

@ -1,38 +1,139 @@
export var CollectionsEnum;
(function (CollectionsEnum) {
CollectionsEnum["OFFERS"] = "Offers";
})(CollectionsEnum || (CollectionsEnum = {}));
export var OfferFieldsEnum;
(function (OfferFieldsEnum) {
OfferFieldsEnum["BOLT12"] = "bolt12";
OfferFieldsEnum["AMOUNTMSAT"] = "amountmSat";
OfferFieldsEnum["AMOUNTMSAT"] = "amountMSat";
OfferFieldsEnum["TITLE"] = "title";
OfferFieldsEnum["VENDOR"] = "vendor";
OfferFieldsEnum["DESCRIPTION"] = "description";
})(OfferFieldsEnum || (OfferFieldsEnum = {}));
export const CollectionFieldsEnum = Object.assign({}, OfferFieldsEnum);
export class Offer {
constructor(bolt12, amountmSat, title, vendor, description, lastUpdatedAt) {
constructor(bolt12, amountMSat, title, vendor, description, lastUpdatedAt) {
this.bolt12 = bolt12;
this.amountmSat = amountmSat;
this.amountMSat = amountMSat;
this.title = title;
this.vendor = vendor;
this.description = description;
this.lastUpdatedAt = lastUpdatedAt;
}
}
export const validateDocument = (collectionName, documentToValidate) => {
switch (collectionName) {
case CollectionsEnum.OFFERS:
return validateOffer(documentToValidate);
case CollectionsEnum.PAGE_SETTINGS:
return validatePageSettings(documentToValidate);
default:
return ({ isValid: false, error: 'Collection does not exist' });
}
};
export const validateOffer = (documentToValidate) => {
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.BOLT12)) {
return ({ isValid: false, error: CollectionFieldsEnum.BOLT12 + 'is mandatory.' });
return ({ isValid: false, error: 'Bolt12 is mandatory.' });
}
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.AMOUNTMSAT)) {
return ({ isValid: false, error: CollectionFieldsEnum.AMOUNTMSAT + 'is mandatory.' });
return ({ isValid: false, error: 'Amount mSat is mandatory.' });
}
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.TITLE)) {
return ({ isValid: false, error: CollectionFieldsEnum.TITLE + 'is mandatory.' });
return ({ isValid: false, error: 'Title is mandatory.' });
}
if ((typeof documentToValidate[CollectionFieldsEnum.AMOUNTMSAT] !== 'number')) {
return ({ isValid: false, error: CollectionFieldsEnum.AMOUNTMSAT + 'should be a number.' });
return ({ isValid: false, error: 'Amount mSat should be a number.' });
}
return ({ isValid: true });
};
export var SortOrderEnum;
(function (SortOrderEnum) {
SortOrderEnum["ASCENDING"] = "asc";
SortOrderEnum["DESCENDING"] = "desc";
})(SortOrderEnum || (SortOrderEnum = {}));
export var PageSettingsFieldsEnum;
(function (PageSettingsFieldsEnum) {
PageSettingsFieldsEnum["PAGE_ID"] = "pageId";
PageSettingsFieldsEnum["TABLES"] = "tables";
})(PageSettingsFieldsEnum || (PageSettingsFieldsEnum = {}));
export var TableSettingsFieldsEnum;
(function (TableSettingsFieldsEnum) {
TableSettingsFieldsEnum["TABLE_ID"] = "tableId";
TableSettingsFieldsEnum["RECORDS_PER_PAGE"] = "recordsPerPage";
TableSettingsFieldsEnum["SORT_BY"] = "sortBy";
TableSettingsFieldsEnum["SORT_ORDER"] = "sortOrder";
TableSettingsFieldsEnum["COLUMN_SELECTION"] = "columnSelection";
TableSettingsFieldsEnum["COLUMN_SELECTION_SM"] = "columnSelectionSM";
})(TableSettingsFieldsEnum || (TableSettingsFieldsEnum = {}));
export class TableSetting {
constructor(tableId, recordsPerPage, sortBy, sortOrder, columnSelection) {
this.tableId = tableId;
this.recordsPerPage = recordsPerPage;
this.sortBy = sortBy;
this.sortOrder = sortOrder;
this.columnSelection = columnSelection;
}
}
export class PageSettings {
constructor(pageId, tables) {
this.pageId = pageId;
this.tables = tables;
}
}
export const validatePageSettings = (documentToValidate) => {
let errorMessages = '';
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.PAGE_ID)) {
errorMessages = errorMessages + 'Page ID is mandatory.';
}
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.TABLES)) {
errorMessages = errorMessages + 'Tables is mandatory.';
}
const tablesMessages = documentToValidate.tables.reduce((tableAcc, table, tableIdx) => {
let errMsg = '';
if (!table.hasOwnProperty(CollectionFieldsEnum.TABLE_ID)) {
errMsg = errMsg + 'Table ID is mandatory.';
}
if (!table.hasOwnProperty(CollectionFieldsEnum.SORT_BY)) {
errMsg = errMsg + 'Sort By is mandatory.';
}
if (!table.hasOwnProperty(CollectionFieldsEnum.SORT_ORDER)) {
errMsg = errMsg + 'Sort Order is mandatory.';
}
if (!table.hasOwnProperty(CollectionFieldsEnum.COLUMN_SELECTION_SM)) {
errMsg = errMsg + 'Column Selection (Mobile) is mandatory.';
}
if (table[CollectionFieldsEnum.COLUMN_SELECTION_SM].length < 1) {
errMsg = errMsg + 'Column Selection (Mobile) should have at least 1 field.';
}
if (table[CollectionFieldsEnum.COLUMN_SELECTION_SM].length > 3) {
errMsg = errMsg + 'Column Selection (Mobile) should have maximum 3 fields.';
}
if (!table.hasOwnProperty(CollectionFieldsEnum.COLUMN_SELECTION)) {
errMsg = errMsg + 'Column Selection (Desktop) is mandatory.';
}
if (table[CollectionFieldsEnum.COLUMN_SELECTION].length < 2) {
errMsg = errMsg + 'Column Selection (Desktop) should have at least 2 fields.';
}
if (errMsg.trim() !== '') {
tableAcc.push({ table: (table.hasOwnProperty(CollectionFieldsEnum.TABLE_ID) ? table[CollectionFieldsEnum.TABLE_ID] : (tableIdx + 1)), message: errMsg });
}
return tableAcc;
}, []);
if (errorMessages.trim() === '' && tablesMessages.length === 0) {
return ({ isValid: true });
}
else {
const errObj = { page: (documentToValidate.hasOwnProperty(CollectionFieldsEnum.PAGE_ID) ? documentToValidate[CollectionFieldsEnum.PAGE_ID] : 'Unknown') };
if (errorMessages.trim() !== '') {
errObj['message'] = errorMessages;
}
if (tablesMessages.length && tablesMessages.length > 0) {
errObj['tables'] = tablesMessages;
}
return ({ isValid: false, error: JSON.stringify(errObj) });
}
};
export var CollectionsEnum;
(function (CollectionsEnum) {
CollectionsEnum["OFFERS"] = "Offers";
CollectionsEnum["PAGE_SETTINGS"] = "PageSettings";
})(CollectionsEnum || (CollectionsEnum = {}));
export const CollectionFieldsEnum = Object.assign(Object.assign(Object.assign({}, OfferFieldsEnum), PageSettingsFieldsEnum), TableSettingsFieldsEnum);
export const LNDCollection = [CollectionsEnum.PAGE_SETTINGS];
export const ECLCollection = [CollectionsEnum.PAGE_SETTINGS];
export const CLNCollection = [CollectionsEnum.PAGE_SETTINGS, CollectionsEnum.OFFERS];

@ -4,12 +4,14 @@ import authenticateRoutes from './authenticate.js';
import boltzRoutes from './boltz.js';
import loopRoutes from './loop.js';
import RTLConfRoutes from './RTLConf.js';
import pageSettingsRoutes from './pageSettings.js';
const router = Router();
const sharedRoutes = [
{ path: '/authenticate', route: authenticateRoutes },
{ path: '/boltz', route: boltzRoutes },
{ path: '/loop', route: loopRoutes },
{ path: '/conf', route: RTLConfRoutes }
{ path: '/conf', route: RTLConfRoutes },
{ path: '/pagesettings', route: pageSettingsRoutes }
];
sharedRoutes.forEach((route) => {
router.use(route.path, route.route);

@ -0,0 +1,8 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getPageSettings, savePageSettings } from '../../controllers/shared/pageSettings.js';
const router = Router();
router.get('/', isAuthenticated, getPageSettings);
router.post('/', isAuthenticated, savePageSettings);
export default router;

@ -24,7 +24,10 @@ export class CommonService {
this.read_dummy_data = false;
this.baseHref = '/rtl';
this.dummy_data_array_from_file = [];
this.MONTHS = [{ name: 'JAN', days: 31 }, { name: 'FEB', days: 28 }, { name: 'MAR', days: 31 }, { name: 'APR', days: 30 }, { name: 'MAY', days: 31 }, { name: 'JUN', days: 30 }, { name: 'JUL', days: 31 }, { name: 'AUG', days: 31 }, { name: 'SEP', days: 30 }, { name: 'OCT', days: 31 }, { name: 'NOV', days: 30 }, { name: 'DEC', days: 31 }];
this.MONTHS = [
{ name: 'JAN', days: 31 }, { name: 'FEB', days: 28 }, { name: 'MAR', days: 31 }, { name: 'APR', days: 30 }, { name: 'MAY', days: 31 }, { name: 'JUN', days: 30 },
{ name: 'JUL', days: 31 }, { name: 'AUG', days: 31 }, { name: 'SEP', days: 30 }, { name: 'OCT', days: 31 }, { name: 'NOV', days: 30 }, { name: 'DEC', days: 31 }
];
this.getSwapServerOptions = (req) => {
const swapOptions = {
url: req.session.selectedNode.swap_server_url,
@ -253,16 +256,26 @@ export class CommonService {
break;
}
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') });
const newErrorObj = {
statusCode: err.statusCode ? err.statusCode : err.status ? err.status : (err.error && err.error.code && err.error.code === 'ECONNREFUSED') ? 503 : 500,
message: (err.error && err.error.message) ? err.error.message : err.message ? err.message : errMsg,
error: ((err.error && err.error.error && err.error.error.error && typeof err.error.error.error === 'string') ? err.error.error.error :
(err.error && err.error.error && typeof err.error.error === 'string') ? err.error.error :
(err.error && err.error.error && err.error.error.message && typeof err.error.error.message === 'string') ? err.error.error.message :
(err.error && err.error.message && typeof err.error.message === 'string') ? err.error.message :
(err.error && typeof err.error === 'string') ? err.error :
(err.message && typeof err.message === 'string') ? err.message : (typeof err === 'string') ? err : 'Unknown Error')
};
let newErrorObj = { statusCode: 500, message: '', error: '' };
if (err.code && err.code === 'ENOENT') {
newErrorObj = {
statusCode: 500,
message: 'No such file or directory ' + (err.path ? err.path : ''),
error: 'No such file or directory ' + (err.path ? err.path : '')
};
}
else {
newErrorObj = {
statusCode: err.statusCode ? err.statusCode : err.status ? err.status : (err.error && err.error.code && err.error.code === 'ECONNREFUSED') ? 503 : 500,
message: (err.error && err.error.message) ? err.error.message : err.message ? err.message : errMsg,
error: ((err.error && err.error.error && err.error.error.error && typeof err.error.error.error === 'string') ? err.error.error.error :
(err.error && err.error.error && typeof err.error.error === 'string') ? err.error.error :
(err.error && err.error.error && err.error.error.message && typeof err.error.error.message === 'string') ? err.error.error.message :
(err.error && err.error.message && typeof err.error.message === 'string') ? err.error.message :
(err.error && typeof err.error === 'string') ? err.error :
(err.message && typeof err.message === 'string') ? err.message : (typeof err === 'string') ? err : 'Unknown Error')
};
}
return newErrorObj;
};
this.getRequestIP = (req) => ((typeof req.headers['x-forwarded-for'] === 'string' && req.headers['x-forwarded-for'].split(',').shift()) ||

@ -71,7 +71,7 @@ export class ConfigService {
}
]
};
if (+process.env.RTL_SSO === 0) {
if (+process.env.RTL_SSO === 0 || configData.SSO.rtlSSO === 0) {
configData['multiPass'] = 'password';
}
return configData;

@ -3,7 +3,7 @@ import { join, dirname, sep } from 'path';
import { fileURLToPath } from 'url';
import { Common } from '../utils/common.js';
import { Logger } from '../utils/logger.js';
import { CollectionsEnum, validateOffer } from '../models/database.model.js';
import { validateDocument, LNDCollection, ECLCollection, CLNCollection } from '../models/database.model.js';
export class DatabaseService {
constructor() {
this.common = Common;
@ -11,33 +11,68 @@ export class DatabaseService {
this.dbDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'database');
this.nodeDatabase = {};
}
loadDatabase(selectedNode) {
loadDatabase(session) {
const { id, selectedNode } = session;
try {
if (!this.nodeDatabase[selectedNode.index]) {
this.nodeDatabase[selectedNode.index] = { adapter: null, data: null };
this.nodeDatabase[selectedNode.index] = { adapter: null, data: {} };
this.nodeDatabase[selectedNode.index].adapter = new DatabaseAdapter(this.dbDirectory, selectedNode, id);
this.fetchNodeData(selectedNode);
this.logger.log({ selectedNode: selectedNode, level: 'DEBUG', fileName: 'Database', msg: 'Database Loaded', data: this.nodeDatabase[selectedNode.index].data });
}
else {
this.nodeDatabase[selectedNode.index].adapter.insertSession(id);
}
this.nodeDatabase[selectedNode.index].adapter = new DatabaseAdapter(this.dbDirectory, 'rtldb', selectedNode);
this.nodeDatabase[selectedNode.index].data = this.nodeDatabase[selectedNode.index].adapter.fetchData();
}
catch (err) {
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: 'Database', msg: 'Database Load Error', error: err });
}
}
create(selectedNode, collectionName, newDocument) {
fetchNodeData(selectedNode) {
switch (selectedNode.ln_implementation) {
case 'CLN':
for (const collectionName in CLNCollection) {
if (CLNCollection.hasOwnProperty(collectionName)) {
this.nodeDatabase[selectedNode.index].data[CLNCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(CLNCollection[collectionName]);
}
}
break;
case 'ECL':
for (const collectionName in ECLCollection) {
if (ECLCollection.hasOwnProperty(collectionName)) {
this.nodeDatabase[selectedNode.index].data[ECLCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(ECLCollection[collectionName]);
}
}
break;
default:
for (const collectionName in LNDCollection) {
if (LNDCollection.hasOwnProperty(collectionName)) {
this.nodeDatabase[selectedNode.index].data[LNDCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(LNDCollection[collectionName]);
}
}
break;
}
}
validateDocument(collectionName, newDocument) {
return new Promise((resolve, reject) => {
const validationRes = validateDocument(collectionName, newDocument);
if (!validationRes.isValid) {
reject(validationRes.error);
}
else {
resolve(true);
}
});
}
insert(selectedNode, collectionName, newCollection) {
return new Promise((resolve, reject) => {
try {
if (!selectedNode || !selectedNode.index) {
reject(new Error('Selected Node Config Not Found.'));
}
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, newDocument);
if (!validationRes.isValid) {
reject(validationRes.error);
}
else {
this.nodeDatabase[selectedNode.index].data[collectionName].push(newDocument);
this.saveDatabase(+selectedNode.index);
resolve(newDocument);
}
this.nodeDatabase[selectedNode.index].data[collectionName] = newCollection;
this.saveDatabase(selectedNode, collectionName);
resolve(this.nodeDatabase[selectedNode.index].data[collectionName]);
}
catch (errRes) {
reject(errRes);
@ -64,23 +99,17 @@ export class DatabaseService {
}
updatedDocument = foundDoc;
}
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, updatedDocument);
if (!validationRes.isValid) {
reject(validationRes.error);
if (foundDocIdx > -1) {
this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument);
}
else {
if (foundDocIdx > -1) {
this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument);
}
else {
if (!this.nodeDatabase[selectedNode.index].data[collectionName]) {
this.nodeDatabase[selectedNode.index].data[collectionName] = [];
}
this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument);
if (!this.nodeDatabase[selectedNode.index].data[collectionName]) {
this.nodeDatabase[selectedNode.index].data[collectionName] = [];
}
this.saveDatabase(+selectedNode.index);
resolve(updatedDocument);
this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument);
}
this.saveDatabase(selectedNode, collectionName);
resolve(updatedDocument);
}
catch (errRes) {
reject(errRes);
@ -105,7 +134,7 @@ export class DatabaseService {
}
});
}
destroy(selectedNode, collectionName, documentFieldName, documentFieldValue) {
remove(selectedNode, collectionName, documentFieldName, documentFieldValue) {
return new Promise((resolve, reject) => {
try {
if (!selectedNode || !selectedNode.index) {
@ -118,7 +147,7 @@ export class DatabaseService {
else {
reject(new Error('Unable to delete, document not found.'));
}
this.saveDatabase(+selectedNode.index);
this.saveDatabase(selectedNode, collectionName);
resolve(documentFieldValue);
}
catch (errRes) {
@ -126,17 +155,10 @@ export class DatabaseService {
}
});
}
validateDocument(collectionName, documentToValidate) {
switch (collectionName) {
case CollectionsEnum.OFFERS:
return validateOffer(documentToValidate);
default:
return ({ isValid: false, error: 'Collection does not exist' });
}
}
saveDatabase(nodeIndex) {
saveDatabase(selectedNode, collectionName) {
const nodeIndex = +selectedNode.index;
try {
if (+nodeIndex < 1) {
if (nodeIndex < 1) {
return true;
}
const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null;
@ -144,69 +166,131 @@ export class DatabaseService {
this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Database Save Error: Selected Node Setup Not Found.' });
throw new Error('Database Save Error: Selected Node Setup Not Found.');
}
this.nodeDatabase[nodeIndex].adapter.saveData(this.nodeDatabase[nodeIndex].data);
this.logger.log({ selectedNode: this.nodeDatabase[nodeIndex].adapter.selNode, level: 'INFO', fileName: 'Database', msg: 'Database Saved' });
this.nodeDatabase[nodeIndex].adapter.saveData(collectionName, this.nodeDatabase[selectedNode.index].data[collectionName]);
this.logger.log({ selectedNode: this.nodeDatabase[nodeIndex].adapter.selNode, level: 'INFO', fileName: 'Database', msg: 'Database Collection ' + collectionName + ' Saved' });
return true;
}
catch (err) {
const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null;
this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Database Save Error', error: err });
return new Error(err);
throw err;
}
}
unloadDatabase(nodeIndex) {
this.saveDatabase(nodeIndex);
this.nodeDatabase[nodeIndex] = null;
unloadDatabase(nodeIndex, sessionID) {
if (nodeIndex > 0) {
if (this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter) {
this.nodeDatabase[nodeIndex].adapter.removeSession(sessionID);
if (this.nodeDatabase[nodeIndex].adapter.userSessions && this.nodeDatabase[nodeIndex].adapter.userSessions.length <= 0) {
delete this.nodeDatabase[nodeIndex];
}
}
}
}
}
export class DatabaseAdapter {
constructor(dbDirectoryPath, fileName, selNode = null) {
constructor(dbDirectoryPath, selNode = null, id = '') {
this.dbDirectoryPath = dbDirectoryPath;
this.fileName = fileName;
this.selNode = selNode;
this.dbFile = '';
this.dbFile = dbDirectoryPath + sep + fileName + '-node-' + selNode.index + '.json';
this.id = id;
this.logger = Logger;
this.common = Common;
this.dbFilePath = '';
this.userSessions = [];
this.dbFilePath = dbDirectoryPath + sep + 'node-' + selNode.index;
// For backward compatibility Start
const oldFilePath = dbDirectoryPath + sep + 'rtldb-node-' + selNode.index + '.json';
if (selNode.ln_implementation === 'CLN' && fs.existsSync(oldFilePath)) {
this.renameOldDB(oldFilePath, selNode);
}
// For backward compatibility End
this.insertSession(id);
}
fetchData() {
renameOldDB(oldFilePath, selNode = null) {
const newFilePath = this.dbFilePath + sep + 'rtldb-' + selNode.ln_implementation + '-Offers.json';
try {
if (!fs.existsSync(this.dbDirectoryPath)) {
fs.mkdirSync(this.dbDirectoryPath);
this.common.createDirectory(this.dbFilePath);
const oldOffers = JSON.parse(fs.readFileSync(oldFilePath, 'utf-8'));
fs.writeFileSync(oldFilePath, JSON.stringify(oldOffers.Offers, null, 2));
fs.renameSync(oldFilePath, newFilePath);
}
catch (err) {
this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Rename Old Database Error', error: err });
}
}
fetchData(collectionName) {
try {
if (!fs.existsSync(this.dbFilePath)) {
this.common.createDirectory(this.dbFilePath);
}
}
catch (err) {
return new Error('Unable to Create Directory Error ' + JSON.stringify(err));
throw new Error(JSON.stringify(err));
}
const collectionFilePath = this.dbFilePath + sep + 'rtldb-' + this.selNode.ln_implementation + '-' + collectionName + '.json';
try {
if (!fs.existsSync(this.dbFile)) {
fs.writeFileSync(this.dbFile, '{}');
if (!fs.existsSync(collectionFilePath)) {
fs.writeFileSync(collectionFilePath, '[]');
}
}
catch (err) {
return new Error('Unable to Create Database File Error ' + JSON.stringify(err));
throw new Error(JSON.stringify(err));
}
try {
const otherFiles = fs.readdirSync(this.dbFilePath);
otherFiles.forEach((oFileName) => {
let collectionValid = false;
switch (this.selNode.ln_implementation) {
case 'CLN':
collectionValid = CLNCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false);
break;
case 'ECL':
collectionValid = ECLCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false);
break;
default:
collectionValid = LNDCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false);
break;
}
if (oFileName.endsWith('.json') && !collectionValid) {
fs.renameSync(this.dbFilePath + sep + oFileName, this.dbFilePath + sep + oFileName + '.tmp');
}
});
}
catch (err) {
this.logger.log({ selectedNode: this.selNode, level: 'ERROR', fileName: 'Database', msg: 'Rename Other Implementation DB Error', error: err });
}
try {
const dataFromFile = fs.readFileSync(this.dbFile, 'utf-8');
return !dataFromFile ? null : JSON.parse(dataFromFile);
const dataFromFile = fs.readFileSync(collectionFilePath, 'utf-8');
const dataObj = !dataFromFile ? null : JSON.parse(dataFromFile);
return dataObj;
}
catch (err) {
return new Error('Database Read Error ' + JSON.stringify(err));
throw new Error(JSON.stringify(err));
}
}
getSelNode() {
return this.selNode;
}
saveData(data) {
saveData(collectionName, collectionData) {
try {
if (data) {
const tempFile = this.dbFile + '.tmp';
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2));
fs.renameSync(tempFile, this.dbFile);
if (collectionData) {
const collectionFilePath = this.dbFilePath + sep + 'rtldb-' + this.selNode.ln_implementation + '-' + collectionName + '.json';
const tempFile = collectionFilePath + '.tmp';
fs.writeFileSync(tempFile, JSON.stringify(collectionData, null, 2));
fs.renameSync(tempFile, collectionFilePath);
}
return true;
}
catch (err) {
return new Error('Database Write Error ' + JSON.stringify(err));
throw err;
}
}
insertSession(id = '') {
if (!this.userSessions.includes(id)) {
this.userSessions.push(id);
}
}
removeSession(sessionID = '') {
this.userSessions.splice(this.userSessions.findIndex((sId) => sId === sessionID), 1);
}
}
export const Database = new DatabaseService();

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">
<meta i18n-content="" name="msapplication-TileColor" content="#da532c">
<meta i18n-content="" name="theme-color" content="#ffffff">
<style>@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}html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:62.5%}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}</style><link rel="stylesheet" href="styles.43515fc39338348b.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.43515fc39338348b.css"></noscript></head>
<style>@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}html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:62.5%}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}</style><link rel="stylesheet" href="styles.74a7770ce3bccfdd.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.74a7770ce3bccfdd.css"></noscript></head>
<body>
<rtl-app></rtl-app>
<script src="runtime.3a8ac8969006b863.js" type="module"></script><script src="polyfills.eddc63f1737a019a.js" type="module"></script><script src="main.0a28b146399d54a7.js" type="module"></script>
<script src="runtime.7fce12dc3c8ea399.js" type="module"></script><script src="polyfills.eddc63f1737a019a.js" type="module"></script><script src="main.a7cf337fd65d6bc7.js" type="module"></script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
(()=>{"use strict";var e,v={},g={};function r(e){var n=g[e];if(void 0!==n)return n.exports;var t=g[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=(n,t,f,d)=>{if(!t){var a=1/0;for(i=0;i<e.length;i++){for(var[t,f,d]=e[i],s=!0,o=0;o<t.length;o++)(!1&d||a>=d)&&Object.keys(r.O).every(b=>r.O[b](t[o]))?t.splice(o--,1):(s=!1,d<a&&(a=d));if(s){e.splice(i--,1);var l=f();void 0!==l&&(n=l)}}return n}d=d||0;for(var i=e.length;i>0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[t,f,d]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>e+"."+{564:"f639cfe4254bb226",636:"167692d028bb7a59",893:"9a615c46b89a5a79",924:"244f3c9394b6cf6d"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="RTLApp:";r.l=(t,f,d,i)=>{if(e[t])e[t].push(f);else{var a,s;if(void 0!==d)for(var o=document.getElementsByTagName("script"),l=0;l<o.length;l++){var u=o[l];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==n+d){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",n+d),a.src=r.tu(t)),e[t]=[f];var c=(m,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(_=>_(b)),m)return m(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=>{"undefined"!=typeof Symbol&&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:n=>n},"undefined"!=typeof trustedTypes&&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=(f,d)=>{var i=r.o(e,f)?e[f]:void 0;if(0!==i)if(i)d.push(i[2]);else if(666!=f){var a=new Promise((u,c)=>i=e[f]=[u,c]);d.push(i[2]=a);var s=r.p+r.u(f),o=new Error;r.l(s,u=>{if(r.o(e,f)&&(0!==(i=e[f])&&(e[f]=void 0),i)){var c=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;o.message="Loading chunk "+f+" failed.\n("+c+": "+p+")",o.name="ChunkLoadError",o.type=c,o.request=p,i[1](o)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var n=(f,d)=>{var o,l,[i,a,s]=d,u=0;if(i.some(p=>0!==e[p])){for(o in a)r.o(a,o)&&(r.m[o]=a[o]);if(s)var c=s(r)}for(f&&f(d);u<i.length;u++)r.o(e,l=i[u])&&e[l]&&e[l][0](),e[l]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})();

@ -0,0 +1 @@
(()=>{"use strict";var e,v={},g={};function r(e){var n=g[e];if(void 0!==n)return n.exports;var t=g[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=(n,t,f,o)=>{if(!t){var a=1/0;for(i=0;i<e.length;i++){for(var[t,f,o]=e[i],s=!0,u=0;u<t.length;u++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[u]))?t.splice(u--,1):(s=!1,o<a&&(a=o));if(s){e.splice(i--,1);var d=f();void 0!==d&&(n=d)}}return n}o=o||0;for(var i=e.length;i>0&&e[i-1][2]>o;i--)e[i]=e[i-1];e[i]=[t,f,o]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>e+"."+{258:"1ef6fd11380d21a0",267:"a6037bebc6ac269d",564:"283bbf915d77d48a",636:"7035b0c0d8db984a"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="RTLApp:";r.l=(t,f,o,i)=>{if(e[t])e[t].push(f);else{var a,s;if(void 0!==o)for(var u=document.getElementsByTagName("script"),d=0;d<u.length;d++){var l=u[d];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==n+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",n+o),a.src=r.tu(t)),e[t]=[f];var c=(m,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(_=>_(b)),m)return m(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=>{"undefined"!=typeof Symbol&&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:n=>n},"undefined"!=typeof trustedTypes&&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=(f,o)=>{var i=r.o(e,f)?e[f]:void 0;if(0!==i)if(i)o.push(i[2]);else if(666!=f){var a=new Promise((l,c)=>i=e[f]=[l,c]);o.push(i[2]=a);var s=r.p+r.u(f),u=new Error;r.l(s,l=>{if(r.o(e,f)&&(0!==(i=e[f])&&(e[f]=void 0),i)){var c=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;u.message="Loading chunk "+f+" failed.\n("+c+": "+p+")",u.name="ChunkLoadError",u.type=c,u.request=p,i[1](u)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var n=(f,o)=>{var u,d,[i,a,s]=o,l=0;if(i.some(p=>0!==e[p])){for(u in a)r.o(a,u)&&(r.m[u]=a[u]);if(s)var c=s(r)}for(f&&f(o);l<i.length;l++)r.o(e,d=i[l])&&e[d]&&e[d][0](),e[d]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(n.bind(null,0)),t.push=n.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

@ -56,7 +56,7 @@ export const getInfo = (req, res, next) => {
req.session.selectedNode.ln_version = body.version || '';
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Core Lightning\'s Websocket Server.' });
clWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
databaseService.loadDatabase(req.session);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(body);
}

@ -29,10 +29,6 @@ export const listInvoices = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List URL', data: options.url });
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: body });
if (body.invoices && body.invoices.length > 0) {
body.invoices = common.sortDescByKey(body.invoices, 'expires_at');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Sorted Invoices List Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'List Invoices Error', req.session.selectedNode);

@ -11,11 +11,8 @@ const databaseService: DatabaseService = Database;
export const listOfferBookmarks = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offer Bookmarks..' });
databaseService.find(req.session.selectedNode, CollectionsEnum.OFFERS).then((offers: Offer[]) => {
databaseService.find(req.session.selectedNode, CollectionsEnum.OFFERS).then((offers: any) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmarks Received', data: offers });
if (offers && offers.length > 0) {
offers = common.sortDescByKey(offers, 'lastUpdatedAt');
}
res.status(200).json(offers);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'Offer Bookmarks Error', req.session.selectedNode);
@ -25,7 +22,7 @@ export const listOfferBookmarks = (req, res, next) => {
export const deleteOfferBookmark = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Deleting Offer Bookmark..' });
databaseService.destroy(req.session.selectedNode, CollectionsEnum.OFFERS, CollectionFieldsEnum.BOLT12, req.params.offerStr).then((deleteRes) => {
databaseService.remove(req.session.selectedNode, CollectionsEnum.OFFERS, CollectionFieldsEnum.BOLT12, req.params.offerStr).then((deleteRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmark Deleted', data: deleteRes });
res.status(204).json(req.params.offerStr);
}).catch((errRes) => {

@ -41,7 +41,6 @@ export const getUTXOs = (req, res, next) => {
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.ln_server_url + '/v1/listFunds';
request(options).then((body) => {
if (body.outputs) { body.outputs = common.sortDescByStrKey(body.outputs, 'status'); }
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Funds List Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {

@ -63,10 +63,6 @@ export const listPayments = (req, res, next) => {
options.url = req.session.selectedNode.ln_server_url + '/v1/pay/listPayments';
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: body.payments });
if (body && body.payments && body.payments.length > 0) {
body.payments = common.sortDescByKey(body.payments, 'created_at');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sorted Payments List Received', data: body.payments });
res.status(200).json(groupBy(body.payments));
}).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode);
@ -95,15 +91,21 @@ export const postPayment = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent', data: body });
if (req.body.paymentType === 'OFFER') {
if (req.body.saveToDB && req.body.bolt12) {
const offerToUpdate: Offer = { bolt12: req.body.bolt12, amountmSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() };
const offerToUpdate: Offer = { bolt12: req.body.bolt12, amountMSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() };
if (req.body.vendor) { offerToUpdate['vendor'] = req.body.vendor; }
if (req.body.description) { offerToUpdate['description'] = req.body.description; }
return databaseService.update(req.session.selectedNode, CollectionsEnum.OFFERS, offerToUpdate, CollectionFieldsEnum.BOLT12, req.body.bolt12).then((updatedOffer) => {
logger.log({ level: 'DEBUG', fileName: 'Offer', msg: 'Offer Updated', data: updatedOffer });
return res.status(201).json({ paymentResponse: body, saveToDBResponse: updatedOffer });
}).catch((errDB) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB update error', error: errDB });
return res.status(201).json({ paymentResponse: body, saveToDBError: errDB });
// eslint-disable-next-line arrow-body-style
return databaseService.validateDocument(CollectionsEnum.OFFERS, offerToUpdate).then((validated) => {
return databaseService.update(req.session.selectedNode, CollectionsEnum.OFFERS, offerToUpdate, CollectionFieldsEnum.BOLT12, req.body.bolt12).then((updatedOffer) => {
logger.log({ level: 'DEBUG', fileName: 'Payments', msg: 'Offer Updated', data: updatedOffer });
return res.status(201).json({ paymentResponse: body, saveToDBResponse: updatedOffer });
}).catch((errDB) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB update error', error: errDB });
return res.status(201).json({ paymentResponse: body, saveToDBError: errDB });
});
}).catch((errValidation) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB validation error', error: errValidation });
return res.status(201).json({ paymentResponse: body, saveToDBError: errValidation });
});
} else {
return res.status(201).json({ paymentResponse: body, saveToDBResponse: 'NA' });

@ -16,9 +16,8 @@ export const getPeers = (req, res, next) => {
peer.alias = peer.id.substring(0, 20);
}
});
const peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers with Alias Received', data: peers });
res.status(200).json(peers);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers with Alias Received', data: body });
res.status(200).json(body || []);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Peers', 'List Peers Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
@ -31,12 +30,11 @@ export const postPeer = (req, res, next) => {
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.ln_server_url + '/v1/peer/connect';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: body });
request.post(options).then((connectRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: connectRes });
options.url = req.session.selectedNode.ln_server_url + '/v1/peer/listPeers';
request(options).then((body) => {
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
peers = common.newestOnTop(peers, 'id', req.body.id);
request(options).then((listPeersRes) => {
const peers = listPeersRes ? common.newestOnTop(listPeersRes, 'id', req.body.id) : [];
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers });
res.status(201).json(peers);
}).catch((errRes) => {

@ -72,9 +72,6 @@ export const arrangePayments = (selNode: CommonSelectedNode, body) => {
if (relayedEle.amountIn) { relayedEle.amountIn = Math.round(relayedEle.amountIn / 1000); }
if (relayedEle.amountOut) { relayedEle.amountOut = Math.round(relayedEle.amountOut / 1000); }
});
payments.sent = common.sortDescByKey(payments.sent, 'firstPartTimestamp');
payments.received = common.sortDescByKey(payments.received, 'firstPartTimestamp');
payments.relayed = common.sortDescByKey(payments.relayed, 'timestamp');
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Fees', msg: 'Arranged Payments Received', data: payments });
return payments;
};

@ -36,7 +36,7 @@ export const getInfo = (req, res, next) => {
body.lnImplementation = 'Eclair';
req.session.selectedNode.ln_version = body.version.split('-')[0] || '';
eclWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
databaseService.loadDatabase(req.session);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(body);
}).catch((errRes) => {

@ -67,10 +67,7 @@ export const listInvoices = (req, res, next) => {
const invoices = (!body[0] || body[0].length <= 0) ? [] : body[0];
pendingInvoices = (!body[1] || body[1].length <= 0) ? [] : body[1];
return Promise.all(invoices?.map((invoice) => getReceivedPaymentInfo(req.session.selectedNode.ln_server_url, invoice))).
then((values) => {
body = common.sortDescByKey(invoices, 'expiresAt');
return res.status(200).json(invoices);
});
then((values) => res.status(200).json(invoices));
});
} else {
return Promise.all([request(options1), request(options2)]).
@ -81,7 +78,6 @@ export const listInvoices = (req, res, next) => {
if (invoices && invoices.length > 0) {
return Promise.all(invoices?.map((invoice) => getReceivedPaymentInfo(req.session.selectedNode.ln_server_url, invoice))).
then((values) => {
body = common.sortDescByKey(invoices, 'expiresAt');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Sorted Invoices List Received', data: invoices });
return res.status(200).json(invoices);
}).

@ -59,7 +59,6 @@ export const getTransactions = (req, res, next) => {
};
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Getting On Chain Transactions Options', data: options.form });
request.post(options).then((body) => {
if (body && body.length > 0) { body = common.sortDescByKey(body, 'timestamp'); }
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'On Chain Transactions Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {

@ -37,7 +37,6 @@ export const getPeers = (req, res, next) => {
peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20);
return peer;
});
body = common.sortDescByStrKey(body, 'alias');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body });
res.status(200).json(body);
});
@ -87,8 +86,7 @@ export const connectPeer = (req, res, next) => {
peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20);
return peer;
});
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
peers = common.newestOnTop(peers, 'nodeId', req.query.nodeId ? req.query.nodeId : req.query.uri ? req.query.uri.substring(0, req.query.uri.indexOf('@')) : '');
const peers = common.newestOnTop(body || [], 'nodeId', req.query.nodeId ? req.query.nodeId : req.query.uri ? req.query.uri.substring(0, req.query.uri.indexOf('@')) : '');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers });
res.status(201).json(peers);
});

@ -40,7 +40,6 @@ export const getAllChannels = (req, res, next) => {
return getAliasForChannel(req.session.selectedNode, channel);
})
).then((values) => {
body.channels = common.sortDescByKey(body.channels, 'balancedness');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sorted Channels List Received', data: body });
return res.status(200).json(body);
}).catch((errRes) => {
@ -72,12 +71,12 @@ export const getPendingChannels = (req, res, next) => {
if (body.pending_open_channels && body.pending_open_channels.length > 0) {
body.pending_open_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
if (body.pending_closing_channels && body.pending_closing_channels.length > 0) {
body.pending_closing_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
if (body.pending_force_closing_channels && body.pending_force_closing_channels.length > 0) {
body.pending_force_closing_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
if (body.pending_closing_channels && body.pending_closing_channels.length > 0) {
body.pending_closing_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
if (body.waiting_close_channels && body.waiting_close_channels.length > 0) {
body.waiting_close_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
}
@ -109,7 +108,6 @@ export const getClosedChannels = (req, res, next) => {
return getAliasForChannel(req.session.selectedNode, channel);
})
).then((values) => {
body.channels = common.sortDescByKey(body.channels, 'close_height');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels List Received', data: body });
return res.status(200).json(body);
}).catch((errRes) => {

@ -40,7 +40,7 @@ export const getInfo = (req, res, next) => {
} else {
req.session.selectedNode.ln_version = body.version.split('-')[0] || '';
lndWsClient.updateSelectedNode(req.session.selectedNode);
databaseService.loadDatabase(req.session.selectedNode);
databaseService.loadDatabase(req.session);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(body);
}

@ -44,7 +44,6 @@ export const listInvoices = (req, res, next) => {
invoice.r_hash = invoice.r_hash ? Buffer.from(invoice.r_hash, 'base64').toString('hex') : '';
invoice.description_hash = invoice.description_hash ? Buffer.from(invoice.description_hash, 'base64').toString('hex') : null;
});
body.invoices = common.sortDescByKey(body.invoices, 'creation_date');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Sorted Invoices List Received', data: body });
res.status(200).json(body);

@ -57,10 +57,6 @@ export const getPayments = (req, res, next) => {
options.url = req.session.selectedNode.ln_server_url + '/v1/payments?max_payments=' + req.query.max_payments + '&index_offset=' + req.query.index_offset + '&reversed=' + req.query.reversed;
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: body });
if (body.payments && body.payments.length > 0) {
body.payments = common.sortDescByKey(body.payments, 'creation_date');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sorted Payments List Received', data: body });
res.status(200).json(body);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode);

@ -27,10 +27,6 @@ export const getPeers = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers List Received', data: body });
const peers = !body.peers ? [] : body.peers;
return Promise.all(peers?.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers with Alias before Sort', data: body });
if (body.peers) {
body.peers = common.sortDescByStrKey(body.peers, 'alias');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body.peers });
res.status(200).json(body.peers);
});
@ -56,7 +52,6 @@ export const postPeer = (req, res, next) => {
const peers = (!body.peers) ? [] : body.peers;
return Promise.all(peers?.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => {
if (body.peers) {
body.peers = common.sortDescByStrKey(body.peers, 'alias');
body.peers = common.newestOnTop(body.peers, 'pub_key', req.body.pubkey);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: body });
}

@ -40,9 +40,6 @@ export const getAllForwardingEvents = (req, start, end, offset, caller, callback
}
if (!body.last_offset_index || body.last_offset_index < offset + num_max_events) {
responseData[caller].last_offset_index = body.last_offset_index ? body.last_offset_index : 0;
if (responseData[caller].forwarding_events) {
responseData[caller].forwarding_events = common.sortDescByKey(responseData[caller].forwarding_events, 'timestamp');
}
return callback(responseData[caller]);
} else {
return getAllForwardingEvents(req, start, end, offset + num_max_events, caller, callback);

@ -12,10 +12,6 @@ export const getTransactions = (req, res, next) => {
options.url = req.session.selectedNode.ln_server_url + '/v1/transactions';
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Transactions', msg: 'Transactions List Received', data: body });
if (body.transactions && body.transactions.length > 0) {
body.transactions = common.sortDescByKey(body.transactions, 'time_stamp');
}
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Transactions', msg: 'Sorted Transactions List Received', data: body.transactions });
res.status(200).json(body.transactions);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Transactions', 'List Transactions Error', req.session.selectedNode);

@ -22,7 +22,7 @@ export const updateSelectedNode = (req, res, next) => {
if (req.headers && req.headers.authorization && req.headers.authorization !== '') {
wsServer.updateLNWSClientDetails(req.session.id, +req.session.selectedNode.index, +req.params.prevNodeIndex);
if (req.params.prevNodeIndex !== -1) {
databaseService.unloadDatabase(req.params.prevNodeIndex);
databaseService.unloadDatabase(req.params.prevNodeIndex, req.session.id);
}
}
const responseVal = !req.session.selectedNode.ln_node ? '' : req.session.selectedNode.ln_node;

@ -120,7 +120,7 @@ export const resetPassword = (req, res, next) => {
export const logoutUser = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Logged out' });
if (req.session.selectedNode && req.session.selectedNode.index) {
databaseService.unloadDatabase(+req.session.selectedNode.index);
databaseService.unloadDatabase(+req.session.selectedNode.index, req.session.id);
}
req.session.destroy((err) => {
res.clearCookie('connect.sid');

@ -219,10 +219,6 @@ export const swaps = (req, res, next) => {
options.url = options.url + '/v1/loop/swaps';
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Swaps Received', data: body });
if (body.swaps && body.swaps.length > 0) {
body.swaps = common.sortDescByKey(body.swaps, 'initiation_time');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Loop', msg: 'Sorted Loop Swaps List Received', data: body });
}
res.status(200).json(body.swaps);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Loop', 'List Swaps Error', req.session.selectedNode);

@ -0,0 +1,36 @@
import { Database, DatabaseService } from '../../utils/database.js';
import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js';
import { CollectionsEnum, PageSettings } from '../../models/database.model.js';
const logger: LoggerService = Logger;
const common: CommonService = Common;
const databaseService: DatabaseService = Database;
export const getPageSettings = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Getting Page Settings..' });
databaseService.find(req.session.selectedNode, CollectionsEnum.PAGE_SETTINGS).then((settings: PageSettings) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Page Settings Received', data: settings });
res.status(200).json(settings);
}).catch((errRes) => {
const err = common.handleError(errRes, 'Page Settings', 'Page Settings Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const savePageSettings = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Saving Page Settings..' });
// eslint-disable-next-line arrow-body-style
return Promise.all(req.body.map((page) => databaseService.validateDocument(CollectionsEnum.PAGE_SETTINGS, page))).then((values) => {
return databaseService.insert(req.session.selectedNode, CollectionsEnum.PAGE_SETTINGS, req.body).then((insertRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Page Settings', msg: 'Page Settings Updated', data: insertRes });
res.status(201).json(insertRes);
}).catch((insertErrRes) => {
const err = common.handleError(insertErrRes, 'Page Settings', 'Page Settings Update Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
}).catch((errRes) => {
const err = common.handleError(errRes, 'Page Settings', 'Page Settings Validation Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

@ -1,26 +1,16 @@
export enum CollectionsEnum {
OFFERS = 'Offers'
}
export type Collections = {
Offers: Offer[];
}
export enum OfferFieldsEnum {
BOLT12 = 'bolt12',
AMOUNTMSAT = 'amountmSat',
AMOUNTMSAT = 'amountMSat',
TITLE = 'title',
VENDOR = 'vendor',
DESCRIPTION = 'description'
}
export const CollectionFieldsEnum = { ...OfferFieldsEnum };
export class Offer {
constructor(
public bolt12: string,
public amountmSat: number,
public amountMSat: number,
public title: string,
public vendor?: string,
public description?: string,
@ -29,18 +19,138 @@ export class Offer {
}
export const validateDocument = (collectionName: CollectionsEnum, documentToValidate: any): any => {
switch (collectionName) {
case CollectionsEnum.OFFERS:
return validateOffer(documentToValidate);
case CollectionsEnum.PAGE_SETTINGS:
return validatePageSettings(documentToValidate);
default:
return ({ isValid: false, error: 'Collection does not exist' });
}
};
export const validateOffer = (documentToValidate): any => {
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.BOLT12)) {
return ({ isValid: false, error: CollectionFieldsEnum.BOLT12 + 'is mandatory.' });
return ({ isValid: false, error: 'Bolt12 is mandatory.' });
}
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.AMOUNTMSAT)) {
return ({ isValid: false, error: CollectionFieldsEnum.AMOUNTMSAT + 'is mandatory.' });
return ({ isValid: false, error: 'Amount mSat is mandatory.' });
}
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.TITLE)) {
return ({ isValid: false, error: CollectionFieldsEnum.TITLE + 'is mandatory.' });
return ({ isValid: false, error: 'Title is mandatory.' });
}
if ((typeof documentToValidate[CollectionFieldsEnum.AMOUNTMSAT] !== 'number')) {
return ({ isValid: false, error: CollectionFieldsEnum.AMOUNTMSAT + 'should be a number.' });
return ({ isValid: false, error: 'Amount mSat should be a number.' });
}
return ({ isValid: true });
};
export enum SortOrderEnum {
ASCENDING = 'asc',
DESCENDING = 'desc'
}
export enum PageSettingsFieldsEnum {
PAGE_ID = 'pageId',
TABLES = 'tables'
}
export enum TableSettingsFieldsEnum {
TABLE_ID = 'tableId',
RECORDS_PER_PAGE = 'recordsPerPage',
SORT_BY = 'sortBy',
SORT_ORDER = 'sortOrder',
COLUMN_SELECTION = 'columnSelection',
COLUMN_SELECTION_SM = 'columnSelectionSM'
}
export class TableSetting {
constructor(
public tableId: string,
public recordsPerPage?: number,
public sortBy?: string,
public sortOrder?: SortOrderEnum,
public columnSelection?: any[]
) { }
}
export class PageSettings {
constructor(
public pageId: string,
public tables: TableSetting[]
) { }
}
export const validatePageSettings = (documentToValidate): any => {
let errorMessages = '';
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.PAGE_ID)) {
errorMessages = errorMessages + 'Page ID is mandatory.';
}
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.TABLES)) {
errorMessages = errorMessages + 'Tables is mandatory.';
}
const tablesMessages = documentToValidate.tables.reduce((tableAcc, table: TableSetting, tableIdx) => {
let errMsg = '';
if (!table.hasOwnProperty(CollectionFieldsEnum.TABLE_ID)) {
errMsg = errMsg + 'Table ID is mandatory.';
}
if (!table.hasOwnProperty(CollectionFieldsEnum.SORT_BY)) {
errMsg = errMsg + 'Sort By is mandatory.';
}
if (!table.hasOwnProperty(CollectionFieldsEnum.SORT_ORDER)) {
errMsg = errMsg + 'Sort Order is mandatory.';
}
if (!table.hasOwnProperty(CollectionFieldsEnum.COLUMN_SELECTION_SM)) {
errMsg = errMsg + 'Column Selection (Mobile) is mandatory.';
}
if (table[CollectionFieldsEnum.COLUMN_SELECTION_SM].length < 1) {
errMsg = errMsg + 'Column Selection (Mobile) should have at least 1 field.';
}
if (table[CollectionFieldsEnum.COLUMN_SELECTION_SM].length > 3) {
errMsg = errMsg + 'Column Selection (Mobile) should have maximum 3 fields.';
}
if (!table.hasOwnProperty(CollectionFieldsEnum.COLUMN_SELECTION)) {
errMsg = errMsg + 'Column Selection (Desktop) is mandatory.';
}
if (table[CollectionFieldsEnum.COLUMN_SELECTION].length < 2) {
errMsg = errMsg + 'Column Selection (Desktop) should have at least 2 fields.';
}
if (errMsg.trim() !== '') {
tableAcc.push({ table: (table.hasOwnProperty(CollectionFieldsEnum.TABLE_ID) ? table[CollectionFieldsEnum.TABLE_ID] : (tableIdx + 1)), message: errMsg });
}
return tableAcc;
}, []);
if (errorMessages.trim() === '' && tablesMessages.length === 0) {
return ({ isValid: true });
} else {
const errObj = { page: (documentToValidate.hasOwnProperty(CollectionFieldsEnum.PAGE_ID) ? documentToValidate[CollectionFieldsEnum.PAGE_ID] : 'Unknown') };
if (errorMessages.trim() !== '') {
errObj['message'] = errorMessages;
}
if (tablesMessages.length && tablesMessages.length > 0) {
errObj['tables'] = tablesMessages;
}
return ({ isValid: false, error: JSON.stringify(errObj) });
}
};
export enum CollectionsEnum {
OFFERS = 'Offers',
PAGE_SETTINGS = 'PageSettings'
}
export type Collections = {
Offers: Offer[];
PageSettings: PageSettings[];
}
export const CollectionFieldsEnum = { ...OfferFieldsEnum, ...PageSettingsFieldsEnum, ...TableSettingsFieldsEnum };
export const LNDCollection = [CollectionsEnum.PAGE_SETTINGS];
export const ECLCollection = [CollectionsEnum.PAGE_SETTINGS];
export const CLNCollection = [CollectionsEnum.PAGE_SETTINGS, CollectionsEnum.OFFERS];

@ -4,6 +4,7 @@ import authenticateRoutes from './authenticate.js';
import boltzRoutes from './boltz.js';
import loopRoutes from './loop.js';
import RTLConfRoutes from './RTLConf.js';
import pageSettingsRoutes from './pageSettings.js';
const router = Router();
@ -11,7 +12,8 @@ const sharedRoutes = [
{ path: '/authenticate', route: authenticateRoutes },
{ path: '/boltz', route: boltzRoutes },
{ path: '/loop', route: loopRoutes },
{ path: '/conf', route: RTLConfRoutes }
{ path: '/conf', route: RTLConfRoutes },
{ path: '/pagesettings', route: pageSettingsRoutes }
];
sharedRoutes.forEach((route) => {

@ -0,0 +1,11 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getPageSettings, savePageSettings } from '../../controllers/shared/pageSettings.js';
const router = Router();
router.get('/', isAuthenticated, getPageSettings);
router.post('/', isAuthenticated, savePageSettings);
export default router;

@ -26,7 +26,10 @@ export class CommonService {
public read_dummy_data = false;
public baseHref = '/rtl';
private dummy_data_array_from_file = [];
private MONTHS = [{ name: 'JAN', days: 31 }, { name: 'FEB', days: 28 }, { name: 'MAR', days: 31 }, { name: 'APR', days: 30 }, { name: 'MAY', days: 31 }, { name: 'JUN', days: 30 }, { name: 'JUL', days: 31 }, { name: 'AUG', days: 31 }, { name: 'SEP', days: 30 }, { name: 'OCT', days: 31 }, { name: 'NOV', days: 30 }, { name: 'DEC', days: 31 }];
private MONTHS = [
{ name: 'JAN', days: 31 }, { name: 'FEB', days: 28 }, { name: 'MAR', days: 31 }, { name: 'APR', days: 30 }, { name: 'MAY', days: 31 }, { name: 'JUN', days: 30 },
{ name: 'JUL', days: 31 }, { name: 'AUG', days: 31 }, { name: 'SEP', days: 30 }, { name: 'OCT', days: 31 }, { name: 'NOV', days: 30 }, { name: 'DEC', days: 31 }
];
constructor() { }
@ -268,18 +271,27 @@ export class CommonService {
break;
}
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') });
const newErrorObj = {
statusCode: err.statusCode ? err.statusCode : err.status ? err.status : (err.error && err.error.code && err.error.code === 'ECONNREFUSED') ? 503 : 500,
message: (err.error && err.error.message) ? err.error.message : err.message ? err.message : errMsg,
error: (
(err.error && err.error.error && err.error.error.error && typeof err.error.error.error === 'string') ? err.error.error.error :
(err.error && err.error.error && typeof err.error.error === 'string') ? err.error.error :
(err.error && err.error.error && err.error.error.message && typeof err.error.error.message === 'string') ? err.error.error.message :
(err.error && err.error.message && typeof err.error.message === 'string') ? err.error.message :
(err.error && typeof err.error === 'string') ? err.error :
(err.message && typeof err.message === 'string') ? err.message : (typeof err === 'string') ? err : 'Unknown Error'
)
};
let newErrorObj = { statusCode: 500, message: '', error: '' };
if (err.code && err.code === 'ENOENT') {
newErrorObj = {
statusCode: 500,
message: 'No such file or directory ' + (err.path ? err.path : ''),
error: 'No such file or directory ' + (err.path ? err.path : '')
};
} else {
newErrorObj = {
statusCode: err.statusCode ? err.statusCode : err.status ? err.status : (err.error && err.error.code && err.error.code === 'ECONNREFUSED') ? 503 : 500,
message: (err.error && err.error.message) ? err.error.message : err.message ? err.message : errMsg,
error: (
(err.error && err.error.error && err.error.error.error && typeof err.error.error.error === 'string') ? err.error.error.error :
(err.error && err.error.error && typeof err.error.error === 'string') ? err.error.error :
(err.error && err.error.error && err.error.error.message && typeof err.error.error.message === 'string') ? err.error.error.message :
(err.error && err.error.message && typeof err.error.message === 'string') ? err.error.message :
(err.error && typeof err.error === 'string') ? err.error :
(err.message && typeof err.message === 'string') ? err.message : (typeof err === 'string') ? err : 'Unknown Error'
)
};
}
return newErrorObj;
};

@ -75,7 +75,7 @@ export class ConfigService {
}
]
};
if (+process.env.RTL_SSO === 0) {
if (+process.env.RTL_SSO === 0 || configData.SSO.rtlSSO === 0) {
configData['multiPass'] = 'password';
}
return configData;

@ -3,7 +3,7 @@ import { join, dirname, sep } from 'path';
import { fileURLToPath } from 'url';
import { Common, CommonService } from '../utils/common.js';
import { Logger, LoggerService } from '../utils/logger.js';
import { Collections, CollectionsEnum, validateOffer } from '../models/database.model.js';
import { Collections, CollectionsEnum, validateDocument, LNDCollection, ECLCollection, CLNCollection } from '../models/database.model.js';
import { CommonSelectedNode } from '../models/config.model.js';
export class DatabaseService {
@ -15,32 +15,70 @@ export class DatabaseService {
constructor() { }
loadDatabase(selectedNode: CommonSelectedNode) {
loadDatabase(session: any) {
const { id, selectedNode } = session;
try {
if (!this.nodeDatabase[selectedNode.index]) {
this.nodeDatabase[selectedNode.index] = { adapter: null, data: null };
this.nodeDatabase[selectedNode.index] = { adapter: null, data: {} };
this.nodeDatabase[selectedNode.index].adapter = new DatabaseAdapter(this.dbDirectory, selectedNode, id);
this.fetchNodeData(selectedNode);
this.logger.log({ selectedNode: selectedNode, level: 'DEBUG', fileName: 'Database', msg: 'Database Loaded', data: this.nodeDatabase[selectedNode.index].data });
} else {
this.nodeDatabase[selectedNode.index].adapter.insertSession(id);
}
this.nodeDatabase[selectedNode.index].adapter = new DatabaseAdapter(this.dbDirectory, 'rtldb', selectedNode);
this.nodeDatabase[selectedNode.index].data = this.nodeDatabase[selectedNode.index].adapter.fetchData();
} catch (err) {
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: 'Database', msg: 'Database Load Error', error: err });
}
}
create(selectedNode: CommonSelectedNode, collectionName: CollectionsEnum, newDocument: any) {
fetchNodeData(selectedNode: CommonSelectedNode) {
switch (selectedNode.ln_implementation) {
case 'CLN':
for (const collectionName in CLNCollection) {
if (CLNCollection.hasOwnProperty(collectionName)) {
this.nodeDatabase[selectedNode.index].data[CLNCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(CLNCollection[collectionName]);
}
}
break;
case 'ECL':
for (const collectionName in ECLCollection) {
if (ECLCollection.hasOwnProperty(collectionName)) {
this.nodeDatabase[selectedNode.index].data[ECLCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(ECLCollection[collectionName]);
}
}
break;
default:
for (const collectionName in LNDCollection) {
if (LNDCollection.hasOwnProperty(collectionName)) {
this.nodeDatabase[selectedNode.index].data[LNDCollection[collectionName]] = this.nodeDatabase[selectedNode.index].adapter.fetchData(LNDCollection[collectionName]);
}
}
break;
}
}
validateDocument(collectionName, newDocument) {
return new Promise((resolve, reject) => {
const validationRes = validateDocument(collectionName, newDocument);
if (!validationRes.isValid) {
reject(validationRes.error);
} else {
resolve(true);
}
});
}
insert(selectedNode: CommonSelectedNode, collectionName: CollectionsEnum, newCollection: any) {
return new Promise((resolve, reject) => {
try {
if (!selectedNode || !selectedNode.index) {
reject(new Error('Selected Node Config Not Found.'));
}
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, newDocument);
if (!validationRes.isValid) {
reject(validationRes.error);
} else {
this.nodeDatabase[selectedNode.index].data[collectionName].push(newDocument);
this.saveDatabase(+selectedNode.index);
resolve(newDocument);
}
this.nodeDatabase[selectedNode.index].data[collectionName] = newCollection;
this.saveDatabase(selectedNode, collectionName);
resolve(this.nodeDatabase[selectedNode.index].data[collectionName]);
} catch (errRes) {
reject(errRes);
}
@ -67,21 +105,16 @@ export class DatabaseService {
}
updatedDocument = foundDoc;
}
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, updatedDocument);
if (!validationRes.isValid) {
reject(validationRes.error);
if (foundDocIdx > -1) {
this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument);
} else {
if (foundDocIdx > -1) {
this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument);
} else {
if (!this.nodeDatabase[selectedNode.index].data[collectionName]) {
this.nodeDatabase[selectedNode.index].data[collectionName] = [];
}
this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument);
if (!this.nodeDatabase[selectedNode.index].data[collectionName]) {
this.nodeDatabase[selectedNode.index].data[collectionName] = [];
}
this.saveDatabase(+selectedNode.index);
resolve(updatedDocument);
this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument);
}
this.saveDatabase(selectedNode, collectionName);
resolve(updatedDocument);
} catch (errRes) {
reject(errRes);
}
@ -105,7 +138,7 @@ export class DatabaseService {
});
}
destroy(selectedNode: CommonSelectedNode, collectionName: CollectionsEnum, documentFieldName: string, documentFieldValue: string) {
remove(selectedNode: CommonSelectedNode, collectionName: CollectionsEnum, documentFieldName: string, documentFieldValue: string) {
return new Promise((resolve, reject) => {
try {
if (!selectedNode || !selectedNode.index) {
@ -117,7 +150,7 @@ export class DatabaseService {
} else {
reject(new Error('Unable to delete, document not found.'));
}
this.saveDatabase(+selectedNode.index);
this.saveDatabase(selectedNode, collectionName);
resolve(documentFieldValue);
} catch (errRes) {
reject(errRes);
@ -125,19 +158,10 @@ export class DatabaseService {
});
}
validateDocument(collectionName: CollectionsEnum, documentToValidate: any) {
switch (collectionName) {
case CollectionsEnum.OFFERS:
return validateOffer(documentToValidate);
default:
return ({ isValid: false, error: 'Collection does not exist' });
}
}
saveDatabase(nodeIndex: number) {
saveDatabase(selectedNode: CommonSelectedNode, collectionName: CollectionsEnum) {
const nodeIndex = +selectedNode.index;
try {
if (+nodeIndex < 1) {
if (nodeIndex < 1) {
return true;
}
const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null;
@ -145,51 +169,103 @@ export class DatabaseService {
this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Database Save Error: Selected Node Setup Not Found.' });
throw new Error('Database Save Error: Selected Node Setup Not Found.');
}
this.nodeDatabase[nodeIndex].adapter.saveData(this.nodeDatabase[nodeIndex].data);
this.logger.log({ selectedNode: this.nodeDatabase[nodeIndex].adapter.selNode, level: 'INFO', fileName: 'Database', msg: 'Database Saved' });
this.nodeDatabase[nodeIndex].adapter.saveData(collectionName, this.nodeDatabase[selectedNode.index].data[collectionName]);
this.logger.log({ selectedNode: this.nodeDatabase[nodeIndex].adapter.selNode, level: 'INFO', fileName: 'Database', msg: 'Database Collection ' + collectionName + ' Saved' });
return true;
} catch (err) {
const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null;
this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Database Save Error', error: err });
return new Error(err);
throw err;
}
}
unloadDatabase(nodeIndex: number) {
this.saveDatabase(nodeIndex);
this.nodeDatabase[nodeIndex] = null;
unloadDatabase(nodeIndex: number, sessionID: string) {
if (nodeIndex > 0) {
if (this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter) {
this.nodeDatabase[nodeIndex].adapter.removeSession(sessionID);
if (this.nodeDatabase[nodeIndex].adapter.userSessions && this.nodeDatabase[nodeIndex].adapter.userSessions.length <= 0) {
delete this.nodeDatabase[nodeIndex];
}
}
}
}
}
export class DatabaseAdapter {
private dbFile = '';
private logger: LoggerService = Logger;
private common: CommonService = Common;
private dbFilePath = '';
private userSessions = [];
constructor(public dbDirectoryPath: string, public fileName: string, private selNode: CommonSelectedNode = null) {
this.dbFile = dbDirectoryPath + sep + fileName + '-node-' + selNode.index + '.json';
constructor(public dbDirectoryPath: string, private selNode: CommonSelectedNode = null, private id: string = '') {
this.dbFilePath = dbDirectoryPath + sep + 'node-' + selNode.index;
// For backward compatibility Start
const oldFilePath = dbDirectoryPath + sep + 'rtldb-node-' + selNode.index + '.json';
if (selNode.ln_implementation === 'CLN' && fs.existsSync(oldFilePath)) { this.renameOldDB(oldFilePath, selNode); }
// For backward compatibility End
this.insertSession(id);
}
fetchData() {
renameOldDB(oldFilePath: string, selNode: CommonSelectedNode = null) {
const newFilePath = this.dbFilePath + sep + 'rtldb-' + selNode.ln_implementation + '-Offers.json';
try {
if (!fs.existsSync(this.dbDirectoryPath)) {
fs.mkdirSync(this.dbDirectoryPath);
this.common.createDirectory(this.dbFilePath);
const oldOffers: any = JSON.parse(fs.readFileSync(oldFilePath, 'utf-8'));
fs.writeFileSync(oldFilePath, JSON.stringify(oldOffers.Offers, null, 2));
fs.renameSync(oldFilePath, newFilePath);
} catch (err) {
this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Rename Old Database Error', error: err });
}
}
fetchData(collectionName: string) {
try {
if (!fs.existsSync(this.dbFilePath)) {
this.common.createDirectory(this.dbFilePath);
}
} catch (err) {
return new Error('Unable to Create Directory Error ' + JSON.stringify(err));
throw new Error(JSON.stringify(err));
}
const collectionFilePath = this.dbFilePath + sep + 'rtldb-' + this.selNode.ln_implementation + '-' + collectionName + '.json';
try {
if (!fs.existsSync(this.dbFile)) {
fs.writeFileSync(this.dbFile, '{}');
if (!fs.existsSync(collectionFilePath)) {
fs.writeFileSync(collectionFilePath, '[]');
}
} catch (err) {
return new Error('Unable to Create Database File Error ' + JSON.stringify(err));
throw new Error(JSON.stringify(err));
}
try {
const otherFiles = fs.readdirSync(this.dbFilePath);
otherFiles.forEach((oFileName) => {
let collectionValid = false;
switch (this.selNode.ln_implementation) {
case 'CLN':
collectionValid = CLNCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false);
break;
case 'ECL':
collectionValid = ECLCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false);
break;
default:
collectionValid = LNDCollection.reduce((acc, collection) => acc || oFileName === ('rtldb-' + this.selNode.ln_implementation + '-' + collection + '.json'), false);
break;
}
if (oFileName.endsWith('.json') && !collectionValid) {
fs.renameSync(this.dbFilePath + sep + oFileName, this.dbFilePath + sep + oFileName + '.tmp');
}
});
} catch (err) {
this.logger.log({ selectedNode: this.selNode, level: 'ERROR', fileName: 'Database', msg: 'Rename Other Implementation DB Error', error: err });
}
try {
const dataFromFile = fs.readFileSync(this.dbFile, 'utf-8');
return !dataFromFile ? null : (<Collections>JSON.parse(dataFromFile));
const dataFromFile = fs.readFileSync(collectionFilePath, 'utf-8');
const dataObj = !dataFromFile ? null : (<Collections>JSON.parse(dataFromFile));
return dataObj;
} catch (err) {
return new Error('Database Read Error ' + JSON.stringify(err));
throw new Error(JSON.stringify(err));
}
}
@ -197,19 +273,30 @@ export class DatabaseAdapter {
return this.selNode;
}
saveData(data: any) {
saveData(collectionName: string, collectionData: any) {
try {
if (data) {
const tempFile = this.dbFile + '.tmp';
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2));
fs.renameSync(tempFile, this.dbFile);
if (collectionData) {
const collectionFilePath = this.dbFilePath + sep + 'rtldb-' + this.selNode.ln_implementation + '-' + collectionName + '.json';
const tempFile = collectionFilePath + '.tmp';
fs.writeFileSync(tempFile, JSON.stringify(collectionData, null, 2));
fs.renameSync(tempFile, collectionFilePath);
}
return true;
} catch (err) {
return new Error('Database Write Error ' + JSON.stringify(err));
throw err;
}
}
insertSession(id: string = '') {
if (!this.userSessions.includes(id)) {
this.userSessions.push(id);
}
}
removeSession(sessionID: string = '') {
this.userSessions.splice(this.userSessions.findIndex((sId) => sId === sessionID), 1);
}
}
export const Database = new DatabaseService();

@ -8,6 +8,7 @@ import { BitcoinConfigComponent } from './shared/components/settings/bitcoin-con
import { NodeConfigComponent } from './shared/components/node-config/node-config.component';
import { LNPConfigComponent } from './shared/components/node-config/lnp-config/lnp-config.component';
import { NodeSettingsComponent } from './shared/components/node-config/node-settings/node-settings.component';
import { PageSettingsComponent } from './shared/components/node-config/page-settings/page-settings.component';
import { ServicesSettingsComponent } from './shared/components/node-config/services-settings/services-settings.component';
import { LoopServiceSettingsComponent } from './shared/components/node-config/services-settings/loop-service-settings/loop-service-settings.component';
import { BoltzServiceSettingsComponent } from './shared/components/node-config/services-settings/boltz-service-settings/boltz-service-settings.component';
@ -20,12 +21,6 @@ import { NotFoundComponent } from './shared/components/not-found/not-found.compo
import { ErrorComponent } from './shared/components/error/error.component';
import { AuthGuard } from './shared/services/auth.guard';
import { ExperimentalSettingsComponent } from './shared/components/node-config/experimental-settings/experimental-settings.component';
import { PeerswapComponent } from './shared/components/ln-services/peerswap/peerswap.component';
import { PeerswapServiceSettingsComponent } from './shared/components/node-config/services-settings/peerswap-service-settings/peerswap-service-settings.component';
import { SwapPeersComponent } from './shared/components/ln-services/peerswap/swap-peers/swap-peers.component';
import { PeerswapsOutComponent } from './shared/components/ln-services/peerswap/swaps-out/swaps-out.component';
import { PeerswapsInComponent } from './shared/components/ln-services/peerswap/swaps-in/swaps-in.component';
import { PeerswapsCancelledComponent } from './shared/components/ln-services/peerswap/swaps-cancelled/swaps-cancelled.component';
export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'login' },
@ -42,14 +37,14 @@ export const routes: Routes = [
},
{
path: 'config', component: NodeConfigComponent, canActivate: [AuthGuard], children: [
{ path: '', pathMatch: 'full', redirectTo: 'layout' },
{ path: 'layout', component: NodeSettingsComponent, canActivate: [AuthGuard] },
{ path: '', pathMatch: 'full', redirectTo: 'applayout' },
{ path: 'applayout', component: NodeSettingsComponent, canActivate: [AuthGuard] },
{ path: 'pglayout', component: PageSettingsComponent, canActivate: [AuthGuard] },
{
path: 'services', component: ServicesSettingsComponent, canActivate: [AuthGuard], children: [
{ path: '', pathMatch: 'full', redirectTo: 'loop' },
{ path: 'loop', component: LoopServiceSettingsComponent, canActivate: [AuthGuard] },
{ path: 'boltz', component: BoltzServiceSettingsComponent, canActivate: [AuthGuard] },
{ path: 'peerswap', component: PeerswapServiceSettingsComponent, canActivate: [AuthGuard] }
{ path: 'boltz', component: BoltzServiceSettingsComponent, canActivate: [AuthGuard] }
]
},
{ path: 'experimental', component: ExperimentalSettingsComponent, canActivate: [AuthGuard] },
@ -62,16 +57,7 @@ export const routes: Routes = [
{ path: 'loop', pathMatch: 'full', redirectTo: 'loop/loopout' },
{ path: 'loop/:selTab', component: LoopComponent },
{ path: 'boltz', pathMatch: 'full', redirectTo: 'boltz/swapout' },
{ path: 'boltz/:selTab', component: BoltzRootComponent },
{
path: 'peerswap', component: PeerswapComponent, canActivate: [AuthGuard], children: [
{ path: '', pathMatch: 'full', redirectTo: 'peers' },
{ path: 'peers', component: SwapPeersComponent, canActivate: [AuthGuard] },
{ path: 'psout', component: PeerswapsOutComponent, canActivate: [AuthGuard] },
{ path: 'psin', component: PeerswapsInComponent, canActivate: [AuthGuard] },
{ path: 'pscancelled', component: PeerswapsCancelledComponent, canActivate: [AuthGuard] }
]
}
{ path: 'boltz/:selTab', component: BoltzRootComponent }
]
},
{ path: 'help', component: HelpComponent },
@ -81,4 +67,4 @@ export const routes: Routes = [
];
// Export const routing: ModuleWithProviders<RouterModule> = RouterModule.forRoot(routes, { enableTracing: true });
export const routing: ModuleWithProviders<RouterModule> = RouterModule.forRoot(routes);
export const routing: ModuleWithProviders<RouterModule> = RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled' });

@ -8,7 +8,7 @@
</div>
<mat-divider [inset]="true"></mat-divider>
<div fxLayout="column" fxFlex="20" class="my-1">
<h4 class="font-bold-500">Short Channel Id</h4>
<h4 class="font-bold-500">Short Channel ID</h4>
<span class="foreground-secondary-text">{{lookupResult[0]?.short_channel_id}}</span>
</div>
<mat-divider [inset]="true"></mat-divider>
@ -89,7 +89,7 @@
</div>
<mat-divider [inset]="true"></mat-divider>
<div fxLayout="column" fxFlex="20" class="my-1">
<h4 class="font-bold-500">Short Channel Id</h4>
<h4 class="font-bold-500">Short Channel ID</h4>
<span class="foreground-secondary-text">{{lookupResult[1]?.short_channel_id}}</span>
</div>
<mat-divider [inset]="true"></mat-divider>

@ -17,7 +17,7 @@
<button class="mr-1" mat-stroked-button color="primary" tabindex="3" type="button" (click)="resetData()">Clear</button>
<button mat-flat-button color="primary" tabindex="4" type="submit" (click)="onLookup()">Lookup</button>
</div>
</form>
</form>
<div fxFlex="100" fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign.gt-sm="space-between center" fxLayoutAlign="start stretch" *ngIf="flgSetLookupValue" class="w-100 mt-2">
<div fxLayout="row" fxFlex="100" fxLayoutAlign="start center">
<span class="page-title font-bold-500">{{lookupFields[selectedFieldId].name}} Details</span>
@ -27,7 +27,7 @@
<span fxFlex="100" *ngSwitchCase="1"><div *ngIf="channelLookupValue.length>0; else errorBlock"><rtl-cln-channel-lookup [lookupResult]="channelLookupValue"></rtl-cln-channel-lookup></div></span>
<span fxFlex="100" *ngSwitchDefault><h3>Error! Unable to find details!</h3></span>
</div>
</div>
</div>
</mat-card-content>
</div>
</div>

@ -8,7 +8,3 @@
margin-bottom: 0;
list-style-type: none;
}
.pl-3 {
padding-left: 3rem;
}

@ -24,7 +24,6 @@ export class CLNLookupsComponent implements OnInit, OnDestroy {
public nodeLookupValue = { nodeid: '' };
public channelLookupValue = [];
public flgSetLookupValue = false;
public temp: any;
public messageObj = [];
public selectedFieldId = 0;
public lookupFields = [

@ -1,5 +1,5 @@
<div fxLayout="column" *ngIf="lookupResult" class="mt-1">
<mat-divider [inset]="true" class="mb-1"></mat-divider>
<mat-divider [inset]="true" class="mb-1"></mat-divider>
<div fxLayout="row">
<div fxFlex="30">
<h4 fxLayoutAlign="start" class="font-bold-500">Alias</h4>
@ -28,21 +28,23 @@
<table mat-table #table [dataSource]="addresses" matSort class="overflow-auto">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
<td mat-cell *matCellDef="let address"> {{address?.type}} </td>
<td mat-cell *matCellDef="let address">{{address?.type}}</td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Address</th>
<td mat-cell *matCellDef="let address"> {{address?.address}} </td>
<td mat-cell *matCellDef="let address">{{address?.address}}</td>
</ng-container>
<ng-container matColumnDef="port">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Port</th>
<td mat-cell *matCellDef="let address"> {{address?.port}} </td>
<td mat-cell *matCellDef="let address">{{address?.port}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="pl-1"><span fxLayoutAlign="end center">Actions</span></th>
<td mat-cell *matCellDef="let address" class="pl-1">
<th mat-header-cell *matHeaderCellDef>
<div class="bordered-box table-actions-select btn-action" fxLayoutAlign="center center">Actions</div>
</th>
<td mat-cell *matCellDef="let address">
<span fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="1" rtlClipboard [payload]="lookupResult?.nodeid + '@' + address.address + ':' + address.port" (copied)="onCopyNodeURI($event)">Copy Node URI</button>
<button mat-stroked-button class="btn-action-copy" color="primary" type="button" tabindex="1" rtlClipboard [payload]="lookupResult?.nodeid + '@' + address.address + ':' + address.port" (copied)="onCopyNodeURI($event)">Copy Node URI</button>
</span>
</td>
</ng-container>

@ -0,0 +1,10 @@
div.bordered-box.table-actions-select.btn-action {
min-width: 13rem;
width: 13rem;
min-height: 3.6rem;
}
button.mat-stroked-button.btn-action-copy {
min-width: 13rem;
width: 13rem;
}

@ -17,7 +17,7 @@ export class CLNNodeLookupComponent implements OnInit {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@Input() lookupResult: LookupNode;
public featureDescriptions: string[] = [];
public addresses: any;
public addresses: any = new MatTableDataSource([]);
public displayedColumns = ['type', 'address', 'port', 'actions'];
constructor(private logger: LoggerService, private snackBar: MatSnackBar) { }

@ -27,42 +27,46 @@
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="qrHops" matSort [ngClass]="{'overflow-auto error-border': flgLoading[0]==='error','overflow-auto': true}">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let hop"> {{hop?.id}} </td>
<th mat-header-cell *matHeaderCellDef mat-sort-header>ID</th>
<td mat-cell *matCellDef="let hop">
<div class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<span class="ellipsis-child">{{hop?.id}}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th>
<td mat-cell *matCellDef="let hop"> {{hop?.alias}} </td>
<th mat-header-cell *matHeaderCellDef mat-sort-header>Alias</th>
<td mat-cell *matCellDef="let hop">
<div class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<span class="ellipsis-child">{{hop?.alias}}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="channel">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Channel </th>
<td mat-cell *matCellDef="let hop"> {{hop?.channel}} </td>
<th mat-header-cell *matHeaderCellDef mat-sort-header>Channel</th>
<td mat-cell *matCellDef="let hop">{{hop?.channel}}</td>
</ng-container>
<ng-container matColumnDef="direction">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Direction </th>
<td mat-cell *matCellDef="let hop"> {{hop?.direction}} </td>
<th mat-header-cell *matHeaderCellDef mat-sort-header>Direction</th>
<td mat-cell *matCellDef="let hop">{{hop?.direction}}</td>
</ng-container>
<ng-container matColumnDef="delay">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Delay </th>
<td mat-cell *matCellDef="let hop"><span fxLayoutAlign="end center"> {{hop?.delay | number}} </span>
</td>
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Delay</th>
<td mat-cell *matCellDef="let hop"><span fxLayoutAlign="end center">{{hop?.delay | number}} </span></td>
</ng-container>
<ng-container matColumnDef="msatoshi">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Amount (Sats) </th>
<td mat-cell *matCellDef="let hop"><span fxLayoutAlign="end center"> {{hop?.msatoshi/1000 | number}}
</span></td>
</ng-container>
<ng-container matColumnDef="amount_msat">
<th mat-header-cell class="pl-4" *matHeaderCellDef mat-sort-header> Amount mSat </th>
<td mat-cell class="pl-4" *matCellDef="let hop"> {{hop?.amount_msat}} </td>
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Amount (Sats)</th>
<td mat-cell *matCellDef="let hop"><span fxLayoutAlign="end center">{{hop?.msatoshi/1000 | number}}</span></td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="pl-4 pr-3"><span fxLayoutAlign="end center">Actions</span></th>
<td mat-cell *matCellDef="let hop" class="pl-4">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onHopClick(hop, $event)">View Info</button>
<th mat-header-cell *matHeaderCellDef>
<div class="bordered-box table-actions-select" fxLayoutAlign="center center">Actions</div>
</th>
<td mat-cell *matCellDef="let hop" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onHopClick(hop, $event)" class="table-actions-button">View Info</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>

@ -1,11 +0,0 @@
.mat-column-actions {
flex: 0 0 5%;
width: 5%;
}
.mat-column-pubkey_alias {
flex: 1 1 25%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

@ -7,13 +7,16 @@ import { MatTableDataSource } from '@angular/material/table';
import { faRoute, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { Routes } from '../../../shared/models/clnModels';
import { AlertTypeEnum, DataTypeEnum, ScreenSizeEnum } from '../../../shared/services/consts-enums-functions';
import { AlertTypeEnum, CLN_DEFAULT_PAGE_SETTINGS, DataTypeEnum, PAGE_SIZE, ScreenSizeEnum, SortOrderEnum } from '../../../shared/services/consts-enums-functions';
import { CommonService } from '../../../shared/services/common.service';
import { CLNEffects } from '../../store/cln.effects';
import { RTLState } from '../../../store/rtl.state';
import { openAlert } from '../../../store/rtl.actions';
import { getQueryRoutes } from '../../store/cln.actions';
import { PageSettings, TableSetting } from '../../../shared/models/pageSettings';
import { clnPageSettings } from '../../store/cln.selector';
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
@Component({
selector: 'rtl-cln-query-routes',
@ -24,38 +27,35 @@ export class CLNQueryRoutesComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild('queryRoutesForm', { static: true }) form: any;
public PAGE_ID = 'graph_lookup';
public tableSetting: TableSetting = { tableId: 'query_routes', recordsPerPage: PAGE_SIZE, sortBy: 'id', sortOrder: SortOrderEnum.ASCENDING };
public destinationPubkey = '';
public amount: number | null = null;
public qrHops: any;
public flgSticky = false;
public qrHops: any = new MatTableDataSource([]);
public displayedColumns: any[] = [];
public flgLoading: Array<Boolean | 'error'> = [false]; // 0: peers
public faRoute = faRoute;
public faExclamationTriangle = faExclamationTriangle;
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private store: Store<RTLState>, private clnEffects: CLNEffects, private commonService: CommonService) {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
this.displayedColumns = ['alias', 'msatoshi', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false;
this.displayedColumns = ['alias', 'direction', 'msatoshi', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['alias', 'direction', 'delay', 'msatoshi', 'actions'];
} else {
this.flgSticky = true;
this.displayedColumns = ['alias', 'channel', 'direction', 'delay', 'msatoshi', 'actions'];
}
}
ngOnInit() {
this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])).
subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => {
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.clnEffects.setQueryRoutesCL.pipe(takeUntil(this.unSubs[1])).subscribe((queryRoute) => {
this.qrHops = new MatTableDataSource([]);
this.qrHops.data = [];
if (queryRoute.routes && queryRoute.routes.length && queryRoute.routes.length > 0) {
this.flgLoading[0] = false;
@ -66,6 +66,7 @@ export class CLNQueryRoutesComponent implements OnInit, OnDestroy {
}
this.qrHops.sort = this.sort;
this.qrHops.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null);
this.qrHops.sort?.sort({ id: this.tableSetting.sortBy, start: this.tableSetting.sortOrder, disableClear: true });
});
}

@ -100,9 +100,9 @@
</button>
<mat-menu #menuTransactions="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
</mat-menu>
</mat-menu>
</ng-template>
</mat-tab>
</mat-tab>
</mat-tab-group>
</span>
<h3 *ngSwitchDefault>Error! Unable to find information!</h3>

@ -9,7 +9,7 @@
<form fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-sm="row wrap" #formAsk="ngForm">
<div fxFlex="100" fxLayout="row" class="alert alert-warn">
<fa-icon [icon]="faExclamationTriangle" class="mr-1 alert-icon"></fa-icon>
<span>Ads should be supplemented with additional research of the nodes, before buying liquidity.</span>
<span>Ads should be supplemented with additional research of the node, before buying liquidity.</span>
</div>
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign.gt-sm="space-between center" fxLayoutAlign="start start" class="page-sub-title-container mt-1">
<div fxFlex="30">
@ -23,8 +23,8 @@
<mat-error *ngIf="!channelAmount">Channel amount is required.</mat-error>
</mat-form-field>
<mat-form-field fxFlex="34">
<input matInput placeholder="Channel Opening Fee Rate (Sats/vByte)" name="channelOpeningFeeRate" [(ngModel)]="channelOpeningFeeRate" (keyup)="onCalculateOpeningFee()" type="number" step="10" tabindex="2" required>
<mat-error *ngIf="!channelOpeningFeeRate">Channel opening fee rate is required.</mat-error>
<input matInput placeholder="Channel Opening Fee Rate (Sats/vByte)" name="channel_opening_feeRate" [(ngModel)]="channel_opening_feeRate" (keyup)="onCalculateOpeningFee()" type="number" step="10" tabindex="2" required>
<mat-error *ngIf="!channel_opening_feeRate">Channel opening fee rate is required.</mat-error>
</mat-form-field>
</div>
</form>
@ -38,11 +38,11 @@
</span>
</div>
<mat-form-field fxFlex="34">
<input matInput placeholder="Node Capacity (Sats)" name="nodeCapacity" [(ngModel)]="nodeCapacity" (keyup)="onFilter()" tabindex="5" type="number" min="0" step="1000">
<mat-error *ngIf="!nodeCapacity">Node capacity is required.</mat-error>
<input matInput placeholder="Node Capacity (Sats)" name="node_capacity" [(ngModel)]="node_capacity" (keyup)="onFilter()" tabindex="5" type="number" min="0" step="1000">
<mat-error *ngIf="!node_capacity">Node capacity is required.</mat-error>
</mat-form-field>
<mat-form-field fxFlex="34">
<input matInput placeholder="Channel Count" name="channelCount" [(ngModel)]="channelCount" (keyup)="onFilter()" type="number" step="1" min="0" tabindex="6">
<input matInput placeholder="Channel Count" name="channel_count" [(ngModel)]="channel_count" (keyup)="onFilter()" type="number" step="1" min="0" tabindex="6">
</mat-form-field>
</div>
</form> -->
@ -51,62 +51,95 @@
<fa-icon [icon]="faUsers" class="page-title-img mr-1"></fa-icon>
<span class="page-title">Liquidity Providing Peers</span>
</div>
<mat-form-field fxFlex="30">
<div fxLayout="row" fxLayoutAlign="start start">
<input matInput (keyup)="applyFilter()" [(ngModel)]="selFilter" placeholder="Filter">
</div>
</mat-form-field>
<div fxFlex="30" fxLayoutAlign.gt-xs="space-between center" fxLayout="row" fxLayoutAlign="space-between stretch">
<mat-form-field fxFlex="49">
<mat-select placeholder="Filter By" tabindex="1" [(ngModel)]="selFilterBy" (selectionChange)="selFilter=''; applyFilter()" name="filterBy">
<mat-option *ngFor="let column of ['all'].concat(displayedColumns.slice(0, -1))" [value]="column">{{getLabel(column)}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="49">
<input matInput [(ngModel)]="selFilter" (input)="applyFilter()" (keyup)="applyFilter()" name="filter" placeholder="Filter">
</mat-form-field>
</div>
</div>
<div [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container">
<mat-progress-bar *ngIf="apiCallStatus.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="liquidityNodes" matSort [ngClass]="{'overflow-auto error-border': errorMessage !== '','overflow-auto': true}">
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th>
<td mat-cell *matCellDef="let lqNode" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '50rem'}" fxLayout="row" fxLayoutAlign="start center">
{{lqNode?.alias}}
<mat-chip-list class="ml-half" aria-label="Address Types">
<mat-chip *ngFor="let addrType of lqNode.address_types" color="primary" selected>
{{addrType === 'tor' ? 'Tor' : addrType === 'ipv' ? 'Clearnet' : addrType}}
</mat-chip>
</mat-chip-list>
<th mat-header-cell *matHeaderCellDef mat-sort-header>Alias</th>
<td mat-cell *matCellDef="let lqNode">
<div class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<span fxLayout="row" fxLayoutAlign="start center" class="ellipsis-child">
{{lqNode?.alias}}
<mat-chip-list class="ml-half" aria-label="Address Types">
<mat-chip *ngFor="let addrType of lqNode.address_types" color="primary" selected>
{{addrType === 'tor' ? 'Tor' : addrType === 'ipv' ? 'Clearnet' : addrType}}
</mat-chip>
</mat-chip-list>
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="capacityChannels">
<th mat-header-cell *matHeaderCellDef> Capacity/Channels </th>
<ng-container matColumnDef="nodeid">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Node ID</th>
<td mat-cell *matCellDef="let lqNode">
{{lqNode?.nodeCapacity/100000000 | number:'1.0-2'}} BTC / {{lqNode?.channelCount | number:'1.0-0'}}
<div class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<span class="ellipsis-child">{{lqNode?.nodeid}}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="leaseFee">
<th mat-header-cell *matHeaderCellDef> Lease Fee </th>
<ng-container matColumnDef="last_timestamp">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last Announcement At</th>
<td mat-cell *matCellDef="let lqNode">{{((lqNode?.last_timestamp * 1000) | date:'dd/MMM/y HH:mm') || '-'}}</td>
</ng-container>
<ng-container matColumnDef="compact_lease">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Compact Lease</th>
<td mat-cell *matCellDef="let lqNode">{{ lqNode?.option_will_fund?.compact_lease }}</td>
</ng-container>
<!-- <ng-container matColumnDef="capacity_channels">
<th mat-header-cell *matHeaderCellDef> Capacity/Channels</th>
<td mat-cell *matCellDef="let lqNode">
{{lqNode?.node_capacity/100000000 | number:'1.0-2'}} BTC / {{lqNode?.channel_count | number:'1.0-0'}}
</td>
</ng-container> -->
<ng-container matColumnDef="lease_fee">
<th mat-header-cell *matHeaderCellDef> Lease Fee</th>
<td mat-cell *matCellDef="let lqNode">
{{lqNode?.option_will_fund?.lease_fee_base_msat/1000 | number:'1.0-0'}} Sats + {{(lqNode?.option_will_fund?.lease_fee_basis/100) | number:'1.2-2'}}%
</td>
</ng-container>
<ng-container matColumnDef="routingFee">
<th mat-header-cell *matHeaderCellDef> Routing Fee </th>
<ng-container matColumnDef="routing_fee">
<th mat-header-cell *matHeaderCellDef> Routing Fee</th>
<td mat-cell *matCellDef="let lqNode">
{{lqNode?.option_will_fund?.channel_fee_max_base_msat/1000 | number:'1.0-0'}} Sats + {{lqNode?.option_will_fund?.channel_fee_max_proportional_thousandths * 1000 | number:'1.0-0'}} ppm
</td>
</ng-container>
<ng-container matColumnDef="channelOpeningFee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Channel Opening Fee </th>
<ng-container matColumnDef="channel_opening_fee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Channel Opening Fee (Sats)</th>
<td mat-cell *matCellDef="let lqNode">
<span fxLayoutAlign="end center">
{{lqNode.channel_opening_fee | number:'1.0-0'}}
</span>
</td>
</ng-container>
<ng-container matColumnDef="funding_weight">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Funding Weight</th>
<td mat-cell *matCellDef="let lqNode">
<span fxLayoutAlign="end center">
{{lqNode.channelOpeningFee | number:'1.0-0'}} Sats
{{lqNode?.option_will_fund?.funding_weight | number:'1.0-0'}}
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3">
<div class="bordered-box table-actions-select">
<th mat-header-cell *matHeaderCellDef>
<div class="bordered-box table-actions-select" fxLayoutAlign="center center">
<mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger>
<mat-option (click)="onDownloadCSV()">Download CSV</mat-option>
</mat-select>
</div>
</th>
<td mat-cell *matCellDef="let lqNode" fxLayoutAlign="end center" class="px-3">
</th>
<td mat-cell *matCellDef="let lqNode" fxLayoutAlign="end center">
<div class="bordered-box table-actions-select" fxLayoutAlign="center center">
<mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger>
@ -126,7 +159,7 @@
</td>
</ng-container>
<tr mat-footer-row *matFooterRowDef="['no_lqNode']" [ngClass]="{'display-none': liquidityNodes?.data && liquidityNodes?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>

@ -1,11 +0,0 @@
.mat-column-alias {
flex: 1 1 20%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-height: 4.8rem;
}
.mat-column-actions {
min-height: 4.8rem;
}

@ -10,7 +10,7 @@ import { faBullhorn, faExclamationTriangle, faUsers } from '@fortawesome/free-so
import { DataService } from '../../../shared/services/data.service';
import { LoggerService } from '../../../shared/services/logger.service';
import { CommonService } from '../../../shared/services/common.service';
import { AlertTypeEnum, APICallStatusEnum, DataTypeEnum, getPaginatorLabel, PAGE_SIZE, PAGE_SIZE_OPTIONS, ScreenSizeEnum, NODE_FEATURES_CLN } from '../../../shared/services/consts-enums-functions';
import { AlertTypeEnum, APICallStatusEnum, DataTypeEnum, getPaginatorLabel, PAGE_SIZE, PAGE_SIZE_OPTIONS, ScreenSizeEnum, NODE_FEATURES_CLN, SortOrderEnum, CLN_DEFAULT_PAGE_SETTINGS, CLN_PAGE_DEFS } from '../../../shared/services/consts-enums-functions';
import { GetInfo, LookupNode } from '../../../shared/models/clnModels';
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
import { openAlert, openConfirmation } from '../../../store/rtl.actions';
@ -18,8 +18,10 @@ import { openAlert, openConfirmation } from '../../../store/rtl.actions';
import { RTLState } from '../../../store/rtl.state';
import { RTLEffects } from '../../../store/rtl.effects';
import { CLNOpenLiquidityChannelComponent } from '../open-liquidity-channel-modal/open-liquidity-channel-modal.component';
import { nodeInfoAndNodeSettingsAndBalance } from '../../store/cln.selector';
import { DecimalPipe } from '@angular/common';
import { clnPageSettings, nodeInfoAndNodeSettingsAndBalance } from '../../store/cln.selector';
import { DatePipe } from '@angular/common';
import { ColumnDefinition, PageSettings, TableSetting } from '../../../shared/models/pageSettings';
import { CamelCaseWithReplacePipe } from '../../../shared/pipes/app.pipe';
@Component({
selector: 'rtl-cln-liquidity-ads-list',
@ -33,6 +35,11 @@ export class CLNLiquidityAdsListComponent implements OnInit, 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 = 'liquidity_ads';
public tableSetting: TableSetting = { tableId: 'liquidity_ads', recordsPerPage: PAGE_SIZE, sortBy: 'channel_opening_fee', sortOrder: SortOrderEnum.ASCENDING };
public askTooltipMsg = '';
public nodesTooltipMsg = '';
public displayedColumns: any[] = [];
@ -42,12 +49,11 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
public totalBalance = 0;
public information: GetInfo;
public channelAmount = 100000;
public channelOpeningFeeRate = 10;
public nodeCapacity = 500000;
public channelCount = 5;
public channel_opening_feeRate = 10;
public node_capacity = 500000;
public channel_count = 5;
public liquidityNodesData: LookupNode[] = [];
public liquidityNodes: any;
public flgSticky = false;
public liquidityNodes: any = new MatTableDataSource([]);
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = '';
@ -56,30 +62,37 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
public selFilter = '';
public apiCallStatus: ApiCallStatusPayload = { status: APICallStatusEnum.INITIATED };
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(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<RTLState>, private dataService: DataService, private commonService: CommonService, private rtlEffects: RTLEffects, private decimalPipe: DecimalPipe) {
constructor(private logger: LoggerService, private store: Store<RTLState>, private dataService: DataService, private commonService: CommonService, private rtlEffects: RTLEffects, private datePipe: DatePipe, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.askTooltipMsg = 'Specify the liquidity requirements for your node: \n 1. Channel Amount - Amount in Sats you need on the channel opened to your node \n 2. Channel opening fee rate - Rate in Sats/vByte that you are willing to pay to open the channel to you';
this.nodesTooltipMsg = 'These nodes are advertising their liquidity offering on the network.\nYou should pay attention to the following aspects to evaluate each node offer: \n- The total bitcoin deployed on the node, the more the better\n';
this.nodesTooltipMsg = this.nodesTooltipMsg + '- The number of channels open on the node, the more the better\n- The channel open fee which the node will charge from you\n- The routing fee which the node will charge on the payments, the lesser the better\n- The reliability of the node, ideally uptime. Refer to the information being provided by the node explorers';
this.nodesTooltipMsg = this.nodesTooltipMsg + '- The number of channels open on the node, the more the better' +
'\n- The channel open fee which the node will charge from you\n- The routing fee which the node will charge on the payments, the lesser the better' +
'\n- The reliability of the node, ideally uptime. Refer to the information being provided by the node explorers';
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
this.displayedColumns = ['alias', 'channelOpeningFee', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false;
this.displayedColumns = ['alias', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['alias', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
} else {
this.flgSticky = true;
this.displayedColumns = ['alias', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
}
}
ngOnInit(): void {
combineLatest([this.store.select(nodeInfoAndNodeSettingsAndBalance), this.dataService.listNetworkNodes('?liquidity_ads=yes')]).pipe(takeUntil(this.unSubs[0])).
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) / 10) + 'rem' : '20rem';
this.logger.info(this.displayedColumns);
});
combineLatest([this.store.select(nodeInfoAndNodeSettingsAndBalance), this.dataService.listNetworkNodes('?liquidity_ads=yes')]).pipe(takeUntil(this.unSubs[1])).
subscribe({
next: ([infoSettingsBalSelector, nodeListRes]) => {
this.information = infoSettingsBalSelector.information;
@ -111,7 +124,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
onCalculateOpeningFee() {
this.liquidityNodesData.forEach((lqNode) => {
if (lqNode.option_will_fund) {
lqNode.channelOpeningFee = (+(lqNode.option_will_fund.lease_fee_base_msat || 0) / 1000) + (this.channelAmount * (+(lqNode.option_will_fund.lease_fee_basis || 0)) / 10000) + ((+(lqNode.option_will_fund.funding_weight || 0) / 4) * this.channelOpeningFeeRate);
lqNode.channel_opening_fee = (+(lqNode.option_will_fund.lease_fee_base_msat || 0) / 1000) + (this.channelAmount * (+(lqNode.option_will_fund.lease_fee_basis || 0)) / 10000) + ((+(lqNode.option_will_fund.funding_weight || 0) / 4) * this.channel_opening_feeRate);
}
});
if (this.paginator) { this.paginator.firstPage(); }
@ -125,21 +138,59 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
this.liquidityNodes.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.liquidityNodes.filterPredicate = (rowData: LookupNode, fltr: string) => {
let rowToFilter = '';
switch (this.selFilterBy) {
case 'all':
rowToFilter = ((rowData.alias) ? rowData.alias.toLocaleLowerCase() : '') + (rowData.channel_opening_fee ? rowData.channel_opening_fee + ' Sats' : '') +
(rowData.option_will_fund?.lease_fee_base_msat ? (rowData.option_will_fund?.lease_fee_base_msat / 1000) + ' Sats' : '') + (rowData.option_will_fund?.lease_fee_basis ? ((rowData.option_will_fund?.lease_fee_basis / 100) + '%') : '') +
(rowData.option_will_fund?.channel_fee_max_base_msat ? (rowData.option_will_fund?.channel_fee_max_base_msat / 1000) + ' Sats' : '') + (rowData.option_will_fund?.channel_fee_max_proportional_thousandths ? (rowData.option_will_fund?.channel_fee_max_proportional_thousandths * 1000) + ' ppm' : '') +
(rowData.address_types ? rowData.address_types.reduce((acc, curr) => acc + (curr === 'tor' ? ' tor' : curr === 'ipv' ? ' clearnet' : (' ' + curr.toLowerCase())), '') : '');
break;
case 'alias':
rowToFilter = ((rowData?.alias?.toLowerCase() || ' ') + rowData?.address_types?.reduce((acc, curr) => acc + (!curr ? '' : (curr === 'ipv' ? 'clearnet' : curr)), ' ')) || '';
break;
case 'last_timestamp':
rowToFilter = this.datePipe.transform(new Date((rowData.last_timestamp || 0) * 1000), 'dd/MMM/y HH:mm')?.toLowerCase() || '';
break;
case 'compact_lease':
rowToFilter = rowData?.option_will_fund?.compact_lease?.toLowerCase() || '';
break;
case 'lease_fee':
rowToFilter = ((((rowData.option_will_fund?.lease_fee_base_msat || 0) / 1000) + ' sats ' || ' ') + (((rowData.option_will_fund?.lease_fee_basis || 0) / 100) + '%')) || '';
break;
case 'routing_fee':
rowToFilter = ((((rowData.option_will_fund?.channel_fee_max_base_msat || 0) / 1000) + ' sats ' || ' ') + (((rowData.option_will_fund?.channel_fee_max_proportional_thousandths || 0) * 1000) + ' ppm')) || '';
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);
};
}
loadLiqNodesTable(liqNodes: LookupNode[]) {
this.liquidityNodes = new MatTableDataSource<LookupNode>([...liqNodes]);
this.liquidityNodes.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null);
this.liquidityNodes.sort = this.sort;
this.liquidityNodes.paginator = this.paginator;
if (this.sort) { this.sort.sort({ id: 'channelOpeningFee', start: 'asc', disableClear: true }); }
this.liquidityNodes.filterPredicate = (node: LookupNode, fltr: string) => {
const newNode = ((node.alias) ? node.alias.toLocaleLowerCase() : '') + (node.channelOpeningFee ? node.channelOpeningFee + ' Sats' : '') +
(node.option_will_fund?.lease_fee_base_msat ? (node.option_will_fund?.lease_fee_base_msat / 1000) + ' Sats' : '') + (node.option_will_fund?.lease_fee_basis ? (this.decimalPipe.transform(node.option_will_fund?.lease_fee_basis / 100, '1.2-2') + '%') : '') +
(node.option_will_fund?.channel_fee_max_base_msat ? (node.option_will_fund?.channel_fee_max_base_msat / 1000) + ' Sats' : '') + (node.option_will_fund?.channel_fee_max_proportional_thousandths ? (node.option_will_fund?.channel_fee_max_proportional_thousandths * 1000) + ' ppm' : '') +
(node.address_types ? node.address_types.reduce((acc, curr) => acc + (curr === 'tor' ? ' tor' : curr === 'ipv' ? ' clearnet' : (' ' + curr.toLowerCase())), '') : '');
return newNode.includes(fltr);
};
this.liquidityNodes.sort?.sort({ id: this.tableSetting.sortBy, start: this.tableSetting.sortOrder, disableClear: true });
this.setFilterPredicate();
this.applyFilter();
// this.liquidityNodes.filterPredicate = (node: LookupNode, fltr: string) => node.channelCount >= this.channelCount && node.nodeCapacity >= this.nodeCapacity;
this.liquidityNodes.paginator = this.paginator;
// this.liquidityNodes.filterPredicate = (rowData: LookupNode, fltr: string) => rowData.channel_count >= this.channel_count && rowData.node_capacity >= this.node_capacity;
// this.onFilter();
}
@ -156,7 +207,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
node: lqNode,
balance: this.totalBalance,
requestedAmount: this.channelAmount,
feeRate: this.channelOpeningFeeRate,
feeRate: this.channel_opening_feeRate,
localAmount: 20000
};
this.store.dispatch(openAlert({
@ -208,7 +259,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
}
}
}));
this.rtlEffects.closeConfirm.pipe(takeUntil(this.unSubs[1])).subscribe((confirmRes) => {
this.rtlEffects.closeConfirm.pipe(takeUntil(this.unSubs[2])).subscribe((confirmRes) => {
if (confirmRes) {
this.onOpenChannel(lqNode);
}
@ -222,8 +273,8 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
}
onFilterReset() {
this.nodeCapacity = 0;
this.channelCount = 0;
this.node_capacity = 0;
this.channel_count = 0;
}
ngOnDestroy() {

@ -30,7 +30,7 @@
</mat-form-field>
</div>
<div fxFlex="100" class="alert alert-info mt-4">
<span>Total cost to lease {{node.channelOpeningFee | number}} (Sats)</span>
<span>Total cost to lease {{node.channel_opening_fee | number}} (Sats)</span>
</div>
<div fxFlex="100" class="alert alert-danger mt-2" *ngIf="channelConnectionError !== ''">
<fa-icon [icon]="faExclamationTriangle" class="mr-1 alert-icon"></fa-icon>
@ -73,15 +73,15 @@
<table mat-table #table [dataSource]="node.addresses" matSort class="overflow-auto">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
<td mat-cell *matCellDef="let address"> {{address?.type}} </td>
<td mat-cell *matCellDef="let address">{{address?.type}}</td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Address</th>
<td mat-cell *matCellDef="let address"> {{address?.address }} </td>
<td mat-cell *matCellDef="let address">{{address?.address }}</td>
</ng-container>
<ng-container matColumnDef="port">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Port</th>
<td mat-cell *matCellDef="let address"> {{address?.port}} </td>
<td mat-cell *matCellDef="let address">{{address?.port}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

@ -55,7 +55,7 @@ describe('CLNOpenLiquidityChannelComponent', () => {
channel_fee_max_proportional_thousandths: 1,
compact_lease: '029a0032000100004e20'
},
channelOpeningFee: 22165
channel_opening_fee: 22165
},
balance: 100000, requestedAmount: 20000, feeRate: 10, localAmount: 20000
}

@ -68,7 +68,7 @@ export class CLNOpenLiquidityChannelComponent implements OnInit, OnDestroy {
}
calculateFee() {
this.node.channelOpeningFee = (+(this.node.option_will_fund?.lease_fee_base_msat || 0) / 1000) + (this.requestedAmount * (+(this.node.option_will_fund?.lease_fee_basis || 0)) / 10000) + ((+(this.node.option_will_fund?.funding_weight || 0) / 4) * this.feeRate);
this.node.channel_opening_fee = (+(this.node.option_will_fund?.lease_fee_base_msat || 0) / 1000) + (this.requestedAmount * (+(this.node.option_will_fund?.lease_fee_basis || 0)) / 10000) + ((+(this.node.option_will_fund?.funding_weight || 0) / 4) * this.feeRate);
}
onOpenChannel(): boolean | void {

@ -15,7 +15,7 @@
<mat-form-field fxFlex="30">
<input matInput [(ngModel)]="transaction.satoshis" placeholder="Amount" name="amount" [type]="flgUseAllBalance ? 'text' : 'number'" [step]="100" [min]="0" tabindex="2" required #amount="ngModel" [disabled]="flgUseAllBalance">
<mat-hint *ngIf="flgUseAllBalance">Amount replaced by UTXO balance</mat-hint>
<span matSuffix> {{selAmountUnit}} </span>
<span matSuffix>{{selAmountUnit}} </span>
<mat-error *ngIf="!transaction.satoshis">{{amountError}}</mat-error>
</mat-form-field>
<mat-form-field fxFlex="10" fxLayoutAlign="start end">

@ -77,7 +77,17 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
public screenSizeEnum = ScreenSizeEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<CLNOnChainSendModalComponent>, @Inject(MAT_DIALOG_DATA) public data: CLNOnChainSendFunds, private logger: LoggerService, private store: Store<RTLState>, private commonService: CommonService, private decimalPipe: DecimalPipe, private actions: Actions, private formBuilder: FormBuilder, private rtlEffects: RTLEffects, private snackBar: MatSnackBar) {
constructor(
public dialogRef: MatDialogRef<CLNOnChainSendModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: CLNOnChainSendFunds,
private logger: LoggerService,
private store: Store<RTLState>,
private commonService: CommonService,
private decimalPipe: DecimalPipe,
private actions: Actions,
private formBuilder: FormBuilder,
private rtlEffects: RTLEffects,
private snackBar: MatSnackBar) {
this.screenSize = this.commonService.getScreenSize();
}
@ -196,7 +206,9 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
this.transaction.minconf = this.sendFundFormGroup.controls.flgMinConf.value ? this.sendFundFormGroup.controls.minConfValue.value : null;
} else {
delete this.transaction.minconf;
this.transaction.feeRate = (this.sendFundFormGroup.controls.selFeeRate.value === 'customperkb' && !this.sendFundFormGroup.controls.flgMinConf.value && this.sendFundFormGroup.controls.customFeeRate.value) ? ((this.sendFundFormGroup.controls.customFeeRate.value * 1000) + 'perkb') : this.sendFundFormGroup.controls.selFeeRate.value;
this.transaction.feeRate = (this.sendFundFormGroup.controls.selFeeRate.value === 'customperkb' &&
!this.sendFundFormGroup.controls.flgMinConf.value && this.sendFundFormGroup.controls.customFeeRate.value) ?
((this.sendFundFormGroup.controls.customFeeRate.value * 1000) + 'perkb') : this.sendFundFormGroup.controls.selFeeRate.value;
}
delete this.transaction.utxos;
this.store.dispatch(setChannelTransaction({ payload: this.transaction }));

@ -4,13 +4,13 @@
<ng-template mat-tab-label>
<span matBadge="{{numUtxos}}" matBadgeOverlap="false" class="tab-badge">UTXOs</span>
</ng-template>
<rtl-cln-on-chain-utxos [utxos]="utxos" [numDustUTXOs]="numDustUtxos" [isDustUTXO]="false" xLayout="row" fxFlex="100"></rtl-cln-on-chain-utxos>
<rtl-cln-on-chain-utxos [numDustUTXOs]="numDustUtxos" [isDustUTXO]="false" [dustAmount]="DUST_AMOUNT" fxLayout="row" fxFlex="100"></rtl-cln-on-chain-utxos>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<span matBadge="{{numDustUtxos}}" matBadgeOverlap="false" class="tab-badge">Dust UTXOs</span>
</ng-template>
<rtl-cln-on-chain-utxos [utxos]="dustUtxos" [numDustUTXOs]="numDustUtxos" [isDustUTXO]="true" fxLayout="row" fxFlex="100"></rtl-cln-on-chain-utxos>
<rtl-cln-on-chain-utxos [numDustUTXOs]="numDustUtxos" [isDustUTXO]="true" [dustAmount]="DUST_AMOUNT" fxLayout="row" fxFlex="100"></rtl-cln-on-chain-utxos>
</mat-tab>
</mat-tab-group>
</div>

@ -1,5 +1,6 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule } from '@ngrx/store';
import { CommonService } from '../../../shared/services/common.service';
import { DataService } from '../../../shared/services/data.service';
@ -24,6 +25,7 @@ describe('CLNUTXOTablesComponent', () => {
imports: [
BrowserAnimationsModule,
SharedModule,
RouterTestingModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer })
],
providers: [

@ -19,10 +19,9 @@ export class CLNUTXOTablesComponent implements OnInit, OnDestroy {
@Input() selectedTableIndex = 0;
@Output() readonly selectedTableIndexChange = new EventEmitter<number>();
public utxos: UTXO[] = [];
public numUtxos = 0;
public dustUtxos: UTXO[] = [];
public numDustUtxos = 0;
public DUST_AMOUNT = 1000;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<RTLState>) { }
@ -31,14 +30,8 @@ export class CLNUTXOTablesComponent implements OnInit, OnDestroy {
this.store.select(utxos).pipe(takeUntil(this.unSubs[0])).
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
if (utxosSeletor.utxos && utxosSeletor.utxos.length > 0) {
this.utxos = utxosSeletor.utxos;
this.numUtxos = this.utxos.length;
this.dustUtxos = utxosSeletor.utxos?.filter((utxo) => +(utxo.value || 0) < 1000);
this.numDustUtxos = this.dustUtxos.length;
}
if (utxosSeletor.utxos && utxosSeletor.utxos.length > 0) {
this.utxos = utxosSeletor.utxos;
this.numUtxos = this.utxos.length;
this.numUtxos = utxosSeletor.utxos.length || 0;
this.numDustUtxos = utxosSeletor.utxos?.filter((utxo) => +(utxo.value || 0) < this.DUST_AMOUNT).length || 0;
}
this.logger.info(utxosSeletor);
});

@ -1,57 +1,94 @@
<div fxLayout="row wrap" fxLayoutAlign="start start" fxLayout.gt-sm="column" fxFlex="100" fxLayoutAlign.gt-sm="start stretch" class="padding-gap-x-large">
<div fxLayout="column" fxLayout.gt-xs="row wrap" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70"></div>
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter()" [(ngModel)]="selFilter" placeholder="Filter">
</mat-form-field>
<div fxLayout="column" fxLayoutAlign="start stretch" fxFlex="100" class="padding-gap-x-large">
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70" fxLayoutAlign="start start" fxLayoutAlign.gt-sm="start center"></div>
<div fxFlex="30" fxLayoutAlign.gt-xs="space-between center" fxLayout="row" fxLayoutAlign="space-between stretch">
<mat-form-field fxFlex="49">
<mat-select placeholder="Filter By" tabindex="1" [(ngModel)]="selFilterBy" (selectionChange)="selFilter=''; applyFilter()" name="filterBy">
<mat-option *ngFor="let column of ['all'].concat(displayedColumns.slice(0, -1))" [value]="column">{{getLabel(column)}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="49">
<input matInput [(ngModel)]="selFilter" (input)="applyFilter()" (keyup)="applyFilter()" name="filter" placeholder="Filter">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutAlign="start start">
<div [perfectScrollbar] class="table-container" fxFlex="100">
<mat-progress-bar *ngIf="apiCallStatus?.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="listUTXOs" matSort [ngClass]="{'overflow-auto error-border': errorMessage !== '','overflow-auto': true}">
<ng-container matColumnDef="is_dust">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before" matTooltip="Dust/Nondust"></th>
<td mat-cell *matCellDef="let utxo">
<span *ngIf="numDustUTXOs > 0 && !isDustUTXO && utxo.value < dustAmount; else emptySpace" matTooltip="Risk of dust attack" matTooltipPosition="right">
<mat-icon fxLayoutAlign="start center" color="warn" class="small-icon">warning</mat-icon>
</span>
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before" matTooltip="Status"></th>
<td mat-cell *matCellDef="let utxo">
<span *ngIf="utxo.status === 'confirmed'" class="dot green" matTooltip="Confirmed" matTooltipPosition="right"></span>
<span *ngIf="utxo.status !== 'confirmed'" class="dot yellow" matTooltip="{{utxo.status | titlecase}}" matTooltipPosition="right"></span>
</td>
</ng-container>
<ng-container matColumnDef="txid">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Transaction ID </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header>Transaction ID</th>
<td mat-cell *matCellDef="let utxo">
<span fxLayout="row" class="ellipsis-parent" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '50rem'}">
<span *ngIf="numDustUTXOs > 0 && !isDustUTXO">
<span *ngIf="utxo.value < 1000; else emptySpace" matTooltip="Risk of dust attack" matTooltipPosition="right">
<mat-icon fxLayoutAlign="start center" color="warn" class="mr-1">warning</mat-icon>
</span>
</span>
<span *ngIf="utxo.status === 'confirmed'" class="dot green" matTooltip="Confirmed" matTooltipPosition="right"></span>
<span *ngIf="utxo.status !== 'confirmed'" class="dot yellow" matTooltip="{{utxo.status | titlecase}}" matTooltipPosition="right"></span>
<span fxLayout="row" class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<span class="ellipsis-child">{{utxo.txid}}</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Address</th>
<td mat-cell *matCellDef="let utxo">
<span fxLayout="row" class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<span class="ellipsis-child">{{utxo.address}}</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="scriptpubkey">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Script Pubkey</th>
<td mat-cell *matCellDef="let utxo">
<span fxLayout="row" class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<span class="ellipsis-child">{{utxo.scriptpubkey}}</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="output">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Output </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Output</th>
<td mat-cell *matCellDef="let utxo"><span fxLayoutAlign="end center">
{{utxo?.output | number}} </span></td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Value (Sats) </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Value (Sats)</th>
<td mat-cell *matCellDef="let utxo">
<span fxLayoutAlign="end center" *ngIf="utxo.value > 0 || utxo.value === 0">{{utxo.value | number}}</span>
<span fxLayoutAlign="end center" class="red" *ngIf="utxo.value < 0">({{utxo.value * -1 | number}})</span>
</td>
</ng-container>
<ng-container matColumnDef="blockheight">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Blockheight </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Blockheight</th>
<td mat-cell *matCellDef="let utxo"><span fxLayoutAlign="end center">
{{utxo?.blockheight | number}} </span></td>
</ng-container>
<ng-container matColumnDef="reserved">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Reserved</th>
<td mat-cell *matCellDef="let utxo">
<span>{{utxo.reserved ? 'Yes' : 'No'}}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3">
<div class="bordered-box table-actions-select">
<th mat-header-cell *matHeaderCellDef>
<div class="bordered-box table-actions-select" fxLayoutAlign="center center">
<mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger>
<mat-option (click)="onDownloadCSV()">Download CSV</mat-option>
</mat-select>
</div>
</th>
<td mat-cell *matCellDef="let utxo" class="pl-3" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onUTXOClick(utxo, $event)">View Info</button>
</th>
<td mat-cell *matCellDef="let utxo" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onUTXOClick(utxo, $event)" class="table-actions-button">View Info</button>
</td>
</ng-container>
<ng-container matColumnDef="no_utxo">
@ -62,7 +99,7 @@
</td>
</ng-container>
<tr mat-footer-row *matFooterRowDef="['no_utxo']" [ngClass]="{'display-none': listUTXOs?.data && listUTXOs?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" [showFirstLastButtons]="screenSize === screenSizeEnum.XS ? false : true" class="mb-1"></mat-paginator>

@ -1,11 +1,9 @@
.mat-column-txid {
flex: 0 0 15%;
width: 15%;
& .ellipsis-parent {
display: flex;
}
.mat-column-is_dust {
max-width: 1.2rem;
width: 1.2rem;
}
.mat-column-actions {
min-height: 4.8rem;
.mat-column-status {
max-width: 1.2rem;
width: 1.2rem;
}

@ -1,5 +1,6 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule } from '@ngrx/store';
import { CommonService } from '../../../../shared/services/common.service';
import { DataService } from '../../../../shared/services/data.service';
@ -23,6 +24,7 @@ describe('CLNOnChainUtxosComponent', () => {
imports: [
BrowserAnimationsModule,
SharedModule,
RouterTestingModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer })
],
providers: [

@ -1,4 +1,5 @@
import { Component, ViewChild, Input, OnChanges, 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 { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@ -7,14 +8,16 @@ import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { UTXO } from '../../../../shared/models/clnModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, APICallStatusEnum } from '../../../../shared/services/consts-enums-functions';
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 { RTLState } from '../../../../store/rtl.state';
import { openAlert } from '../../../../store/rtl.actions';
import { utxos } from '../../../store/cln.selector';
import { clnPageSettings, utxos } from '../../../store/cln.selector';
import { ColumnDefinition, PageSettings, TableSetting } from '../../../../shared/models/pageSettings';
import { CamelCaseWithReplacePipe } from '../../../../shared/pipes/app.pipe';
@Component({
selector: 'rtl-cln-on-chain-utxos',
@ -24,16 +27,22 @@ import { utxos } from '../../../store/cln.selector';
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('UTXOs') }
]
})
export class CLNOnChainUtxosComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
export class CLNOnChainUtxosComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | undefined;
@Input() numDustUTXOs = 0;
@Input() isDustUTXO = false;
@Input() utxos: UTXO[];
@Input() dustAmount = 1000;
public nodePageDefs = CLN_PAGE_DEFS;
public selFilterBy = 'all';
public colWidth = '20rem';
public PAGE_ID = 'on_chain';
public tableSetting: TableSetting = { tableId: 'utxos', recordsPerPage: PAGE_SIZE, sortBy: 'status', sortOrder: SortOrderEnum.DESCENDING };
public displayedColumns: any[] = [];
public listUTXOs: any;
public flgSticky = false;
public utxos: UTXO[];
public dustUtxos: UTXO[];
public listUTXOs: any = new MatTableDataSource([]);
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = '';
@ -42,53 +51,66 @@ export class CLNOnChainUtxosComponent implements OnInit, OnChanges, AfterViewIni
public selFilter = '';
public apiCallStatus: ApiCallStatusPayload | null = null;
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [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>) {
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private router: Router, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
this.displayedColumns = ['txid', 'value', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false;
this.displayedColumns = ['txid', 'output', 'value', 'blockheight', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['txid', 'output', 'value', 'blockheight', 'actions'];
} else {
this.flgSticky = true;
this.displayedColumns = ['txid', 'output', 'value', 'blockheight', 'actions'];
}
}
ngOnInit() {
this.store.select(utxos).pipe(takeUntil(this.unSubs[0])).
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.tableSetting.tableId = this.isDustUTXO ? 'dust_utxos' : 'utxos';
this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])).
subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = utxosSeletor.apiCallStatus;
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.unshift('status');
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) / 10) + 'rem' : '20rem';
this.logger.info(this.displayedColumns);
});
this.store.select(utxos).pipe(takeUntil(this.unSubs[1])).
subscribe((utxosSelector: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = utxosSelector.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;
}
this.logger.info(utxosSeletor);
if (utxosSelector.utxos && utxosSelector.utxos.length > 0) {
this.dustUtxos = utxosSelector.utxos?.filter((utxo) => +(utxo.value || 0) < this.dustAmount);
this.utxos = utxosSelector.utxos;
if (this.isDustUTXO) {
if (this.dustUtxos && this.dustUtxos.length > 0 && this.sort && this.paginator && this.displayedColumns.length > 0) {
this.loadUTXOsTable(this.dustUtxos);
}
} else {
this.displayedColumns.unshift('is_dust');
if (this.utxos && this.utxos.length > 0 && this.sort && this.paginator && this.displayedColumns.length > 0) {
this.loadUTXOsTable(this.utxos);
}
}
}
this.logger.info(utxosSelector);
});
}
ngAfterViewInit() {
if (this.utxos && this.utxos.length > 0 && this.sort && this.paginator) {
if (this.utxos && this.utxos.length > 0 && this.sort && this.paginator && this.displayedColumns.length > 0) {
this.loadUTXOsTable(this.utxos);
}
}
ngOnChanges() {
if (this.utxos && this.utxos.length > 0) {
this.loadUTXOsTable(this.utxos);
}
}
applyFilter() {
this.listUTXOs.filter = this.selFilter.trim().toLowerCase();
}
onUTXOClick(selUtxo: UTXO, event: any) {
const reorderedUTXO = [
[{ key: 'txid', value: selUtxo.txid, title: 'Transaction ID', width: 100 }],
@ -109,12 +131,51 @@ export class CLNOnChainUtxosComponent implements OnInit, OnChanges, AfterViewIni
}));
}
applyFilter() {
this.listUTXOs.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, '_') : column === 'is_dust' ? 'Dust' : this.commonService.titleCase(column);
}
setFilterPredicate() {
this.listUTXOs.filterPredicate = (rowData: UTXO, fltr: string) => {
let rowToFilter = '';
switch (this.selFilterBy) {
case 'all':
rowToFilter = JSON.stringify(rowData).toLowerCase();
break;
case 'is_dust':
rowToFilter = (rowData?.value || 0) < this.dustAmount ? 'dust' : 'nondust';
break;
case 'status':
rowToFilter = rowData?.status?.toLowerCase() || '';
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 (this.selFilterBy === 'is_dust' || this.selFilterBy === 'status') ? rowToFilter.indexOf(fltr) === 0 : rowToFilter.includes(fltr);
};
}
loadUTXOsTable(utxos: any[]) {
this.listUTXOs = new MatTableDataSource<UTXO>([...utxos]);
this.listUTXOs.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null);
this.listUTXOs.sortingDataAccessor = (data: UTXO, sortHeaderId: string) => {
switch (sortHeaderId) {
case 'is_dust': return +(data.value || 0) < this.dustAmount;
default: return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null;
}
};
this.listUTXOs.sort = this.sort;
this.listUTXOs.filterPredicate = (utxo: UTXO, fltr: string) => JSON.stringify(utxo).toLowerCase().includes(fltr);
this.listUTXOs.sort?.sort({ id: this.tableSetting.sortBy, start: this.tableSetting.sortOrder, disableClear: true });
this.listUTXOs.paginator = this.paginator;
this.setFilterPredicate();
this.applyFilter();
this.logger.info(this.listUTXOs);
}

@ -75,7 +75,7 @@
<div *ngIf="showAdvanced">
<div fxLayout="row">
<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>
<span class="foreground-secondary-text">{{channel.funding_txid}}</span>
</div>
</div>

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

Loading…
Cancel
Save