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", "curly": "error",
"no-unused-expressions": "error", "no-unused-expressions": "error",
"strict": "error", "strict": "error",
"max-len": ["error", { "code": 450 }], "max-len": ["error", { "code": 320 }],
"no-multiple-empty-lines": "error", "no-multiple-empty-lines": "error",
"no-trailing-spaces": "error", "no-trailing-spaces": "error",
"quote-props": ["error", "as-needed"], "quote-props": ["error", "as-needed"],

@ -62,7 +62,7 @@ export const getInfo = (req, res, next) => {
req.session.selectedNode.ln_version = body.version || ''; 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.' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Core Lightning\'s Websocket Server.' });
clWsClient.updateSelectedNode(req.session.selectedNode); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List URL', data: options.url });
request(options).then((body) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: 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); res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'List Invoices Error', req.session.selectedNode); 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..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offer Bookmarks..' });
databaseService.find(req.session.selectedNode, CollectionsEnum.OFFERS).then((offers) => { 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 }); 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); res.status(200).json(offers);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'Offer Bookmarks Error', req.session.selectedNode); 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) => { export const deleteOfferBookmark = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Deleting Offer Bookmark..' }); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmark Deleted', data: deleteRes });
res.status(204).json(req.params.offerStr); res.status(204).json(req.params.offerStr);
}).catch((errRes) => { }).catch((errRes) => {

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

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

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

@ -88,9 +88,6 @@ export const arrangePayments = (selNode, body) => {
relayedEle.amountOut = Math.round(relayedEle.amountOut / 1000); 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 }); logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Fees', msg: 'Arranged Payments Received', data: payments });
return payments; return payments;
}; };

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

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

@ -37,7 +37,6 @@ export const getPeers = (req, res, next) => {
peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20); peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20);
return peer; return peer;
}); });
body = common.sortDescByStrKey(body, 'alias');
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body });
res.status(200).json(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); peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20);
return peer; return peer;
}); });
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : []; const peers = common.newestOnTop(body || [], 'nodeId', req.query.nodeId ? req.query.nodeId : req.query.uri ? req.query.uri.substring(0, req.query.uri.indexOf('@')) : '');
peers = common.newestOnTop(peers, '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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers });
res.status(201).json(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); channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
return getAliasForChannel(req.session.selectedNode, channel); return getAliasForChannel(req.session.selectedNode, channel);
})).then((values) => { })).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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sorted Channels List Received', data: body });
return res.status(200).json(body); return res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {
@ -73,11 +72,11 @@ export const getPendingChannels = (req, res, next) => {
if (body.pending_open_channels && body.pending_open_channels.length > 0) { 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))); (_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) { 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) { 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))); (_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; channel.close_type = (!channel.close_type) ? 'COOPERATIVE_CLOSE' : channel.close_type;
return getAliasForChannel(req.session.selectedNode, channel); return getAliasForChannel(req.session.selectedNode, channel);
})).then((values) => { })).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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels List Received', data: body });
return res.status(200).json(body); return res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {

@ -45,7 +45,7 @@ export const getInfo = (req, res, next) => {
else { else {
req.session.selectedNode.ln_version = body.version.split('-')[0] || ''; req.session.selectedNode.ln_version = body.version.split('-')[0] || '';
lndWsClient.updateSelectedNode(req.session.selectedNode); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(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.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; 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Sorted Invoices List Received', data: body });
res.status(200).json(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; 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) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: 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); res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers List Received', data: body });
const peers = !body.peers ? [] : body.peers; 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) => { 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body.peers });
res.status(200).json(body.peers); res.status(200).json(body.peers);
}); });
@ -56,7 +52,6 @@ export const postPeer = (req, res, next) => {
const peers = (!body.peers) ? [] : body.peers; 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) => { return Promise.all(peers === null || peers === void 0 ? void 0 : peers.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => {
if (body.peers) { if (body.peers) {
body.peers = common.sortDescByStrKey(body.peers, 'alias');
body.peers = common.newestOnTop(body.peers, 'pub_key', req.body.pubkey); 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 }); 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) { 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; 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]); return callback(responseData[caller]);
} }
else { else {

@ -13,10 +13,6 @@ export const getTransactions = (req, res, next) => {
options.url = req.session.selectedNode.ln_server_url + '/v1/transactions'; options.url = req.session.selectedNode.ln_server_url + '/v1/transactions';
request(options).then((body) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Transactions', msg: 'Transactions List Received', data: 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); res.status(200).json(body.transactions);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Transactions', 'List Transactions Error', req.session.selectedNode); 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 !== '') { if (req.headers && req.headers.authorization && req.headers.authorization !== '') {
wsServer.updateLNWSClientDetails(req.session.id, +req.session.selectedNode.index, +req.params.prevNodeIndex); wsServer.updateLNWSClientDetails(req.session.id, +req.session.selectedNode.index, +req.params.prevNodeIndex);
if (req.params.prevNodeIndex !== -1) { 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; 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) => { export const logoutUser = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Logged out' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Logged out' });
if (req.session.selectedNode && req.session.selectedNode.index) { 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) => { req.session.destroy((err) => {
res.clearCookie('connect.sid'); res.clearCookie('connect.sid');

@ -216,10 +216,6 @@ export const swaps = (req, res, next) => {
options.url = options.url + '/v1/loop/swaps'; options.url = options.url + '/v1/loop/swaps';
request(options).then((body) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Swaps Received', data: 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); res.status(200).json(body.swaps);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Loop', 'List Swaps Error', req.session.selectedNode); 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; export var OfferFieldsEnum;
(function (OfferFieldsEnum) { (function (OfferFieldsEnum) {
OfferFieldsEnum["BOLT12"] = "bolt12"; OfferFieldsEnum["BOLT12"] = "bolt12";
OfferFieldsEnum["AMOUNTMSAT"] = "amountmSat"; OfferFieldsEnum["AMOUNTMSAT"] = "amountMSat";
OfferFieldsEnum["TITLE"] = "title"; OfferFieldsEnum["TITLE"] = "title";
OfferFieldsEnum["VENDOR"] = "vendor"; OfferFieldsEnum["VENDOR"] = "vendor";
OfferFieldsEnum["DESCRIPTION"] = "description"; OfferFieldsEnum["DESCRIPTION"] = "description";
})(OfferFieldsEnum || (OfferFieldsEnum = {})); })(OfferFieldsEnum || (OfferFieldsEnum = {}));
export const CollectionFieldsEnum = Object.assign({}, OfferFieldsEnum);
export class Offer { export class Offer {
constructor(bolt12, amountmSat, title, vendor, description, lastUpdatedAt) { constructor(bolt12, amountMSat, title, vendor, description, lastUpdatedAt) {
this.bolt12 = bolt12; this.bolt12 = bolt12;
this.amountmSat = amountmSat; this.amountMSat = amountMSat;
this.title = title; this.title = title;
this.vendor = vendor; this.vendor = vendor;
this.description = description; this.description = description;
this.lastUpdatedAt = lastUpdatedAt; 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) => { export const validateOffer = (documentToValidate) => {
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.BOLT12)) { 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)) { 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)) { 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')) { 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 }); 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 boltzRoutes from './boltz.js';
import loopRoutes from './loop.js'; import loopRoutes from './loop.js';
import RTLConfRoutes from './RTLConf.js'; import RTLConfRoutes from './RTLConf.js';
import pageSettingsRoutes from './pageSettings.js';
const router = Router(); const router = Router();
const sharedRoutes = [ const sharedRoutes = [
{ path: '/authenticate', route: authenticateRoutes }, { path: '/authenticate', route: authenticateRoutes },
{ path: '/boltz', route: boltzRoutes }, { path: '/boltz', route: boltzRoutes },
{ path: '/loop', route: loopRoutes }, { path: '/loop', route: loopRoutes },
{ path: '/conf', route: RTLConfRoutes } { path: '/conf', route: RTLConfRoutes },
{ path: '/pagesettings', route: pageSettingsRoutes }
]; ];
sharedRoutes.forEach((route) => { sharedRoutes.forEach((route) => {
router.use(route.path, route.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.read_dummy_data = false;
this.baseHref = '/rtl'; this.baseHref = '/rtl';
this.dummy_data_array_from_file = []; 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) => { this.getSwapServerOptions = (req) => {
const swapOptions = { const swapOptions = {
url: req.session.selectedNode.swap_server_url, url: req.session.selectedNode.swap_server_url,
@ -253,16 +256,26 @@ export class CommonService {
break; break;
} }
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') }); this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') });
const newErrorObj = { let newErrorObj = { statusCode: 500, message: '', error: '' };
statusCode: err.statusCode ? err.statusCode : err.status ? err.status : (err.error && err.error.code && err.error.code === 'ECONNREFUSED') ? 503 : 500, if (err.code && err.code === 'ENOENT') {
message: (err.error && err.error.message) ? err.error.message : err.message ? err.message : errMsg, newErrorObj = {
error: ((err.error && err.error.error && err.error.error.error && typeof err.error.error.error === 'string') ? err.error.error.error : statusCode: 500,
(err.error && err.error.error && typeof err.error.error === 'string') ? err.error.error : message: 'No such file or directory ' + (err.path ? err.path : ''),
(err.error && err.error.error && err.error.error.message && typeof err.error.error.message === 'string') ? err.error.error.message : error: 'No such file or directory ' + (err.path ? err.path : '')
(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') 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; return newErrorObj;
}; };
this.getRequestIP = (req) => ((typeof req.headers['x-forwarded-for'] === 'string' && req.headers['x-forwarded-for'].split(',').shift()) || 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'; configData['multiPass'] = 'password';
} }
return configData; return configData;

@ -3,7 +3,7 @@ import { join, dirname, sep } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { Common } from '../utils/common.js'; import { Common } from '../utils/common.js';
import { Logger } from '../utils/logger.js'; import { Logger } from '../utils/logger.js';
import { CollectionsEnum, validateOffer } from '../models/database.model.js'; import { validateDocument, LNDCollection, ECLCollection, CLNCollection } from '../models/database.model.js';
export class DatabaseService { export class DatabaseService {
constructor() { constructor() {
this.common = Common; this.common = Common;
@ -11,33 +11,68 @@ export class DatabaseService {
this.dbDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'database'); this.dbDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'database');
this.nodeDatabase = {}; this.nodeDatabase = {};
} }
loadDatabase(selectedNode) { loadDatabase(session) {
const { id, selectedNode } = session;
try { try {
if (!this.nodeDatabase[selectedNode.index]) { 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) { catch (err) {
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: 'Database', msg: 'Database Load Error', error: 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) => { return new Promise((resolve, reject) => {
try { try {
if (!selectedNode || !selectedNode.index) { if (!selectedNode || !selectedNode.index) {
reject(new Error('Selected Node Config Not Found.')); reject(new Error('Selected Node Config Not Found.'));
} }
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, newDocument); this.nodeDatabase[selectedNode.index].data[collectionName] = newCollection;
if (!validationRes.isValid) { this.saveDatabase(selectedNode, collectionName);
reject(validationRes.error); resolve(this.nodeDatabase[selectedNode.index].data[collectionName]);
}
else {
this.nodeDatabase[selectedNode.index].data[collectionName].push(newDocument);
this.saveDatabase(+selectedNode.index);
resolve(newDocument);
}
} }
catch (errRes) { catch (errRes) {
reject(errRes); reject(errRes);
@ -64,23 +99,17 @@ export class DatabaseService {
} }
updatedDocument = foundDoc; updatedDocument = foundDoc;
} }
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, updatedDocument); if (foundDocIdx > -1) {
if (!validationRes.isValid) { this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument);
reject(validationRes.error);
} }
else { else {
if (foundDocIdx > -1) { if (!this.nodeDatabase[selectedNode.index].data[collectionName]) {
this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument); this.nodeDatabase[selectedNode.index].data[collectionName] = [];
}
else {
if (!this.nodeDatabase[selectedNode.index].data[collectionName]) {
this.nodeDatabase[selectedNode.index].data[collectionName] = [];
}
this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument);
} }
this.saveDatabase(+selectedNode.index); this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument);
resolve(updatedDocument);
} }
this.saveDatabase(selectedNode, collectionName);
resolve(updatedDocument);
} }
catch (errRes) { catch (errRes) {
reject(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) => { return new Promise((resolve, reject) => {
try { try {
if (!selectedNode || !selectedNode.index) { if (!selectedNode || !selectedNode.index) {
@ -118,7 +147,7 @@ export class DatabaseService {
else { else {
reject(new Error('Unable to delete, document not found.')); reject(new Error('Unable to delete, document not found.'));
} }
this.saveDatabase(+selectedNode.index); this.saveDatabase(selectedNode, collectionName);
resolve(documentFieldValue); resolve(documentFieldValue);
} }
catch (errRes) { catch (errRes) {
@ -126,17 +155,10 @@ export class DatabaseService {
} }
}); });
} }
validateDocument(collectionName, documentToValidate) { saveDatabase(selectedNode, collectionName) {
switch (collectionName) { const nodeIndex = +selectedNode.index;
case CollectionsEnum.OFFERS:
return validateOffer(documentToValidate);
default:
return ({ isValid: false, error: 'Collection does not exist' });
}
}
saveDatabase(nodeIndex) {
try { try {
if (+nodeIndex < 1) { if (nodeIndex < 1) {
return true; return true;
} }
const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null; 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.' }); 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.'); throw new Error('Database Save Error: Selected Node Setup Not Found.');
} }
this.nodeDatabase[nodeIndex].adapter.saveData(this.nodeDatabase[nodeIndex].data); 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 Saved' }); this.logger.log({ selectedNode: this.nodeDatabase[nodeIndex].adapter.selNode, level: 'INFO', fileName: 'Database', msg: 'Database Collection ' + collectionName + ' Saved' });
return true; return true;
} }
catch (err) { catch (err) {
const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null; 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 }); this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Database Save Error', error: err });
return new Error(err); throw err;
} }
} }
unloadDatabase(nodeIndex) { unloadDatabase(nodeIndex, sessionID) {
this.saveDatabase(nodeIndex); if (nodeIndex > 0) {
this.nodeDatabase[nodeIndex] = null; 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 { export class DatabaseAdapter {
constructor(dbDirectoryPath, fileName, selNode = null) { constructor(dbDirectoryPath, selNode = null, id = '') {
this.dbDirectoryPath = dbDirectoryPath; this.dbDirectoryPath = dbDirectoryPath;
this.fileName = fileName;
this.selNode = selNode; this.selNode = selNode;
this.dbFile = ''; this.id = id;
this.dbFile = dbDirectoryPath + sep + fileName + '-node-' + selNode.index + '.json'; 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 { try {
if (!fs.existsSync(this.dbDirectoryPath)) { this.common.createDirectory(this.dbFilePath);
fs.mkdirSync(this.dbDirectoryPath); 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) { 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 { try {
if (!fs.existsSync(this.dbFile)) { if (!fs.existsSync(collectionFilePath)) {
fs.writeFileSync(this.dbFile, '{}'); fs.writeFileSync(collectionFilePath, '[]');
} }
} }
catch (err) { 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 { try {
const dataFromFile = fs.readFileSync(this.dbFile, 'utf-8'); const dataFromFile = fs.readFileSync(collectionFilePath, 'utf-8');
return !dataFromFile ? null : JSON.parse(dataFromFile); const dataObj = !dataFromFile ? null : JSON.parse(dataFromFile);
return dataObj;
} }
catch (err) { catch (err) {
return new Error('Database Read Error ' + JSON.stringify(err)); throw new Error(JSON.stringify(err));
} }
} }
getSelNode() { getSelNode() {
return this.selNode; return this.selNode;
} }
saveData(data) { saveData(collectionName, collectionData) {
try { try {
if (data) { if (collectionData) {
const tempFile = this.dbFile + '.tmp'; const collectionFilePath = this.dbFilePath + sep + 'rtldb-' + this.selNode.ln_implementation + '-' + collectionName + '.json';
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2)); const tempFile = collectionFilePath + '.tmp';
fs.renameSync(tempFile, this.dbFile); fs.writeFileSync(tempFile, JSON.stringify(collectionData, null, 2));
fs.renameSync(tempFile, collectionFilePath);
} }
return true; return true;
} }
catch (err) { 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(); 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"> <link i18n-rel="" rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5">
<meta i18n-content="" name="msapplication-TileColor" content="#da532c"> <meta i18n-content="" name="msapplication-TileColor" content="#da532c">
<meta i18n-content="" name="theme-color" content="#ffffff"> <meta i18n-content="" name="theme-color" content="#ffffff">
<style>@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> <body>
<rtl-app></rtl-app> <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> </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 || ''; 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.' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Core Lightning\'s Websocket Server.' });
clWsClient.updateSelectedNode(req.session.selectedNode); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List URL', data: options.url });
request(options).then((body) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Invoice', msg: 'Invoices List Received', data: 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); res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'List Invoices Error', req.session.selectedNode); 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) => { export const listOfferBookmarks = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Getting Offer Bookmarks..' }); 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 }); 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); res.status(200).json(offers);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Offers', 'Offer Bookmarks Error', req.session.selectedNode); 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) => { export const deleteOfferBookmark = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Deleting Offer Bookmark..' }); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Bookmark Deleted', data: deleteRes });
res.status(204).json(req.params.offerStr); res.status(204).json(req.params.offerStr);
}).catch((errRes) => { }).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 }); } 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'; options.url = req.session.selectedNode.ln_server_url + '/v1/listFunds';
request(options).then((body) => { 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'OnChain', msg: 'Funds List Received', data: body });
res.status(200).json(body); res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {

@ -63,10 +63,6 @@ export const listPayments = (req, res, next) => {
options.url = req.session.selectedNode.ln_server_url + '/v1/pay/listPayments'; options.url = req.session.selectedNode.ln_server_url + '/v1/pay/listPayments';
request(options).then((body) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: body.payments }); 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)); res.status(200).json(groupBy(body.payments));
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent', data: body });
if (req.body.paymentType === 'OFFER') { if (req.body.paymentType === 'OFFER') {
if (req.body.saveToDB && req.body.bolt12) { if (req.body.saveToDB && req.body.bolt12) {
const offerToUpdate: Offer = { bolt12: req.body.bolt12, amountmSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() }; const offerToUpdate: Offer = { bolt12: req.body.bolt12, amountMSat: (req.body.zeroAmtOffer ? 0 : req.body.amount), title: req.body.title, lastUpdatedAt: new Date(Date.now()).getTime() };
if (req.body.vendor) { offerToUpdate['vendor'] = req.body.vendor; } if (req.body.vendor) { offerToUpdate['vendor'] = req.body.vendor; }
if (req.body.description) { offerToUpdate['description'] = req.body.description; } 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) => { // eslint-disable-next-line arrow-body-style
logger.log({ level: 'DEBUG', fileName: 'Offer', msg: 'Offer Updated', data: updatedOffer }); return databaseService.validateDocument(CollectionsEnum.OFFERS, offerToUpdate).then((validated) => {
return res.status(201).json({ paymentResponse: body, saveToDBResponse: updatedOffer }); return databaseService.update(req.session.selectedNode, CollectionsEnum.OFFERS, offerToUpdate, CollectionFieldsEnum.BOLT12, req.body.bolt12).then((updatedOffer) => {
}).catch((errDB) => { logger.log({ level: 'DEBUG', fileName: 'Payments', msg: 'Offer Updated', data: updatedOffer });
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Payments', msg: 'Offer DB update error', error: errDB }); return res.status(201).json({ paymentResponse: body, saveToDBResponse: updatedOffer });
return res.status(201).json({ paymentResponse: body, saveToDBError: errDB }); }).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 { } else {
return res.status(201).json({ paymentResponse: body, saveToDBResponse: 'NA' }); 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); 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: body });
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers with Alias Received', data: peers }); res.status(200).json(body || []);
res.status(200).json(peers);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Peers', 'List Peers Error', req.session.selectedNode); const err = common.handleError(errRes, 'Peers', 'List Peers Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error }); return res.status(err.statusCode).json({ message: err.message, error: err.error });
@ -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 }); } 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.url = req.session.selectedNode.ln_server_url + '/v1/peer/connect';
options.body = req.body; options.body = req.body;
request.post(options).then((body) => { request.post(options).then((connectRes) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peer Connected', data: body }); 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'; options.url = req.session.selectedNode.ln_server_url + '/v1/peer/listPeers';
request(options).then((body) => { request(options).then((listPeersRes) => {
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : []; const peers = listPeersRes ? common.newestOnTop(listPeersRes, 'id', req.body.id) : [];
peers = common.newestOnTop(peers, 'id', req.body.id);
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Peers List after Connect Received', data: peers });
res.status(201).json(peers); res.status(201).json(peers);
}).catch((errRes) => { }).catch((errRes) => {

@ -72,9 +72,6 @@ export const arrangePayments = (selNode: CommonSelectedNode, body) => {
if (relayedEle.amountIn) { relayedEle.amountIn = Math.round(relayedEle.amountIn / 1000); } if (relayedEle.amountIn) { relayedEle.amountIn = Math.round(relayedEle.amountIn / 1000); }
if (relayedEle.amountOut) { relayedEle.amountOut = Math.round(relayedEle.amountOut / 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 }); logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Fees', msg: 'Arranged Payments Received', data: payments });
return payments; return payments;
}; };

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

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

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

@ -40,7 +40,6 @@ export const getAllChannels = (req, res, next) => {
return getAliasForChannel(req.session.selectedNode, channel); return getAliasForChannel(req.session.selectedNode, channel);
}) })
).then((values) => { ).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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sorted Channels List Received', data: body });
return res.status(200).json(body); return res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {
@ -72,12 +71,12 @@ export const getPendingChannels = (req, res, next) => {
if (body.pending_open_channels && body.pending_open_channels.length > 0) { 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))); 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) { 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))); 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) { 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))); 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); return getAliasForChannel(req.session.selectedNode, channel);
}) })
).then((values) => { ).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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels List Received', data: body });
return res.status(200).json(body); return res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {

@ -40,7 +40,7 @@ export const getInfo = (req, res, next) => {
} else { } else {
req.session.selectedNode.ln_version = body.version.split('-')[0] || ''; req.session.selectedNode.ln_version = body.version.split('-')[0] || '';
lndWsClient.updateSelectedNode(req.session.selectedNode); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'GetInfo', msg: 'Node Information Received', data: body });
return res.status(200).json(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.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; 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Sorted Invoices List Received', data: body });
res.status(200).json(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; 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) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Payment List Received', data: 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); res.status(200).json(body);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'List Payments Error', req.session.selectedNode); 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Peers', msg: 'Peers List Received', data: body });
const peers = !body.peers ? [] : body.peers; const peers = !body.peers ? [] : body.peers;
return Promise.all(peers?.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => { 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 }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Peers', msg: 'Sorted Peers List Received', data: body.peers });
res.status(200).json(body.peers); res.status(200).json(body.peers);
}); });
@ -56,7 +52,6 @@ export const postPeer = (req, res, next) => {
const peers = (!body.peers) ? [] : body.peers; const peers = (!body.peers) ? [] : body.peers;
return Promise.all(peers?.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => { return Promise.all(peers?.map((peer) => getAliasForPeers(req.session.selectedNode, peer))).then((values) => {
if (body.peers) { if (body.peers) {
body.peers = common.sortDescByStrKey(body.peers, 'alias');
body.peers = common.newestOnTop(body.peers, 'pub_key', req.body.pubkey); 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 }); 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) { 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; 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]); return callback(responseData[caller]);
} else { } else {
return getAllForwardingEvents(req, start, end, offset + num_max_events, caller, callback); 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'; options.url = req.session.selectedNode.ln_server_url + '/v1/transactions';
request(options).then((body) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Transactions', msg: 'Transactions List Received', data: 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); res.status(200).json(body.transactions);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Transactions', 'List Transactions Error', req.session.selectedNode); 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 !== '') { if (req.headers && req.headers.authorization && req.headers.authorization !== '') {
wsServer.updateLNWSClientDetails(req.session.id, +req.session.selectedNode.index, +req.params.prevNodeIndex); wsServer.updateLNWSClientDetails(req.session.id, +req.session.selectedNode.index, +req.params.prevNodeIndex);
if (req.params.prevNodeIndex !== -1) { 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; 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) => { export const logoutUser = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Logged out' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Authenticate', msg: 'Logged out' });
if (req.session.selectedNode && req.session.selectedNode.index) { 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) => { req.session.destroy((err) => {
res.clearCookie('connect.sid'); res.clearCookie('connect.sid');

@ -219,10 +219,6 @@ export const swaps = (req, res, next) => {
options.url = options.url + '/v1/loop/swaps'; options.url = options.url + '/v1/loop/swaps';
request(options).then((body) => { request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Loop', msg: 'Loop Swaps Received', data: 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); res.status(200).json(body.swaps);
}).catch((errRes) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Loop', 'List Swaps Error', req.session.selectedNode); 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 { export enum OfferFieldsEnum {
BOLT12 = 'bolt12', BOLT12 = 'bolt12',
AMOUNTMSAT = 'amountmSat', AMOUNTMSAT = 'amountMSat',
TITLE = 'title', TITLE = 'title',
VENDOR = 'vendor', VENDOR = 'vendor',
DESCRIPTION = 'description' DESCRIPTION = 'description'
} }
export const CollectionFieldsEnum = { ...OfferFieldsEnum };
export class Offer { export class Offer {
constructor( constructor(
public bolt12: string, public bolt12: string,
public amountmSat: number, public amountMSat: number,
public title: string, public title: string,
public vendor?: string, public vendor?: string,
public description?: 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 => { export const validateOffer = (documentToValidate): any => {
if (!documentToValidate.hasOwnProperty(CollectionFieldsEnum.BOLT12)) { 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)) { 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)) { 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')) { 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 }); 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 boltzRoutes from './boltz.js';
import loopRoutes from './loop.js'; import loopRoutes from './loop.js';
import RTLConfRoutes from './RTLConf.js'; import RTLConfRoutes from './RTLConf.js';
import pageSettingsRoutes from './pageSettings.js';
const router = Router(); const router = Router();
@ -11,7 +12,8 @@ const sharedRoutes = [
{ path: '/authenticate', route: authenticateRoutes }, { path: '/authenticate', route: authenticateRoutes },
{ path: '/boltz', route: boltzRoutes }, { path: '/boltz', route: boltzRoutes },
{ path: '/loop', route: loopRoutes }, { path: '/loop', route: loopRoutes },
{ path: '/conf', route: RTLConfRoutes } { path: '/conf', route: RTLConfRoutes },
{ path: '/pagesettings', route: pageSettingsRoutes }
]; ];
sharedRoutes.forEach((route) => { 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 read_dummy_data = false;
public baseHref = '/rtl'; public baseHref = '/rtl';
private dummy_data_array_from_file = []; 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() { } constructor() { }
@ -268,18 +271,27 @@ export class CommonService {
break; break;
} }
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') }); this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: fileName, msg: errMsg, error: (typeof err === 'object' ? JSON.stringify(err) : (typeof err === 'string') ? err : 'Unknown Error') });
const newErrorObj = { let newErrorObj = { statusCode: 500, message: '', error: '' };
statusCode: err.statusCode ? err.statusCode : err.status ? err.status : (err.error && err.error.code && err.error.code === 'ECONNREFUSED') ? 503 : 500, if (err.code && err.code === 'ENOENT') {
message: (err.error && err.error.message) ? err.error.message : err.message ? err.message : errMsg, newErrorObj = {
error: ( statusCode: 500,
(err.error && err.error.error && err.error.error.error && typeof err.error.error.error === 'string') ? err.error.error.error : message: 'No such file or directory ' + (err.path ? err.path : ''),
(err.error && err.error.error && typeof err.error.error === 'string') ? err.error.error : error: 'No such file or directory ' + (err.path ? err.path : '')
(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 : } else {
(err.error && typeof err.error === 'string') ? err.error : newErrorObj = {
(err.message && typeof err.message === 'string') ? err.message : (typeof err === 'string') ? err : 'Unknown Error' 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; 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'; configData['multiPass'] = 'password';
} }
return configData; return configData;

@ -3,7 +3,7 @@ import { join, dirname, sep } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { Common, CommonService } from '../utils/common.js'; import { Common, CommonService } from '../utils/common.js';
import { Logger, LoggerService } from '../utils/logger.js'; import { Logger, LoggerService } from '../utils/logger.js';
import { Collections, CollectionsEnum, 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'; import { CommonSelectedNode } from '../models/config.model.js';
export class DatabaseService { export class DatabaseService {
@ -15,32 +15,70 @@ export class DatabaseService {
constructor() { } constructor() { }
loadDatabase(selectedNode: CommonSelectedNode) { loadDatabase(session: any) {
const { id, selectedNode } = session;
try { try {
if (!this.nodeDatabase[selectedNode.index]) { 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) { } catch (err) {
this.logger.log({ selectedNode: selectedNode, level: 'ERROR', fileName: 'Database', msg: 'Database Load Error', error: 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) => { return new Promise((resolve, reject) => {
try { try {
if (!selectedNode || !selectedNode.index) { if (!selectedNode || !selectedNode.index) {
reject(new Error('Selected Node Config Not Found.')); reject(new Error('Selected Node Config Not Found.'));
} }
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, newDocument); this.nodeDatabase[selectedNode.index].data[collectionName] = newCollection;
if (!validationRes.isValid) { this.saveDatabase(selectedNode, collectionName);
reject(validationRes.error); resolve(this.nodeDatabase[selectedNode.index].data[collectionName]);
} else {
this.nodeDatabase[selectedNode.index].data[collectionName].push(newDocument);
this.saveDatabase(+selectedNode.index);
resolve(newDocument);
}
} catch (errRes) { } catch (errRes) {
reject(errRes); reject(errRes);
} }
@ -67,21 +105,16 @@ export class DatabaseService {
} }
updatedDocument = foundDoc; updatedDocument = foundDoc;
} }
const validationRes = this.validateDocument(CollectionsEnum.OFFERS, updatedDocument); if (foundDocIdx > -1) {
if (!validationRes.isValid) { this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument);
reject(validationRes.error);
} else { } else {
if (foundDocIdx > -1) { if (!this.nodeDatabase[selectedNode.index].data[collectionName]) {
this.nodeDatabase[selectedNode.index].data[collectionName].splice(foundDocIdx, 1, updatedDocument); this.nodeDatabase[selectedNode.index].data[collectionName] = [];
} else {
if (!this.nodeDatabase[selectedNode.index].data[collectionName]) {
this.nodeDatabase[selectedNode.index].data[collectionName] = [];
}
this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument);
} }
this.saveDatabase(+selectedNode.index); this.nodeDatabase[selectedNode.index].data[collectionName].push(updatedDocument);
resolve(updatedDocument);
} }
this.saveDatabase(selectedNode, collectionName);
resolve(updatedDocument);
} catch (errRes) { } catch (errRes) {
reject(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) => { return new Promise((resolve, reject) => {
try { try {
if (!selectedNode || !selectedNode.index) { if (!selectedNode || !selectedNode.index) {
@ -117,7 +150,7 @@ export class DatabaseService {
} else { } else {
reject(new Error('Unable to delete, document not found.')); reject(new Error('Unable to delete, document not found.'));
} }
this.saveDatabase(+selectedNode.index); this.saveDatabase(selectedNode, collectionName);
resolve(documentFieldValue); resolve(documentFieldValue);
} catch (errRes) { } catch (errRes) {
reject(errRes); reject(errRes);
@ -125,19 +158,10 @@ export class DatabaseService {
}); });
} }
validateDocument(collectionName: CollectionsEnum, documentToValidate: any) { saveDatabase(selectedNode: CommonSelectedNode, collectionName: CollectionsEnum) {
switch (collectionName) { const nodeIndex = +selectedNode.index;
case CollectionsEnum.OFFERS:
return validateOffer(documentToValidate);
default:
return ({ isValid: false, error: 'Collection does not exist' });
}
}
saveDatabase(nodeIndex: number) {
try { try {
if (+nodeIndex < 1) { if (nodeIndex < 1) {
return true; return true;
} }
const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null; 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.' }); 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.'); throw new Error('Database Save Error: Selected Node Setup Not Found.');
} }
this.nodeDatabase[nodeIndex].adapter.saveData(this.nodeDatabase[nodeIndex].data); 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 Saved' }); this.logger.log({ selectedNode: this.nodeDatabase[nodeIndex].adapter.selNode, level: 'INFO', fileName: 'Database', msg: 'Database Collection ' + collectionName + ' Saved' });
return true; return true;
} catch (err) { } catch (err) {
const selNode = this.nodeDatabase[nodeIndex] && this.nodeDatabase[nodeIndex].adapter && this.nodeDatabase[nodeIndex].adapter.selNode ? this.nodeDatabase[nodeIndex].adapter.selNode : null; 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 }); this.logger.log({ selectedNode: selNode, level: 'ERROR', fileName: 'Database', msg: 'Database Save Error', error: err });
return new Error(err); throw err;
} }
} }
unloadDatabase(nodeIndex: number) { unloadDatabase(nodeIndex: number, sessionID: string) {
this.saveDatabase(nodeIndex); if (nodeIndex > 0) {
this.nodeDatabase[nodeIndex] = null; 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 { 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) { constructor(public dbDirectoryPath: string, private selNode: CommonSelectedNode = null, private id: string = '') {
this.dbFile = dbDirectoryPath + sep + fileName + '-node-' + selNode.index + '.json'; 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 { try {
if (!fs.existsSync(this.dbDirectoryPath)) { this.common.createDirectory(this.dbFilePath);
fs.mkdirSync(this.dbDirectoryPath); 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) { } 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 { try {
if (!fs.existsSync(this.dbFile)) { if (!fs.existsSync(collectionFilePath)) {
fs.writeFileSync(this.dbFile, '{}'); fs.writeFileSync(collectionFilePath, '[]');
} }
} catch (err) { } 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 { try {
const dataFromFile = fs.readFileSync(this.dbFile, 'utf-8'); const dataFromFile = fs.readFileSync(collectionFilePath, 'utf-8');
return !dataFromFile ? null : (<Collections>JSON.parse(dataFromFile)); const dataObj = !dataFromFile ? null : (<Collections>JSON.parse(dataFromFile));
return dataObj;
} catch (err) { } 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; return this.selNode;
} }
saveData(data: any) { saveData(collectionName: string, collectionData: any) {
try { try {
if (data) { if (collectionData) {
const tempFile = this.dbFile + '.tmp'; const collectionFilePath = this.dbFilePath + sep + 'rtldb-' + this.selNode.ln_implementation + '-' + collectionName + '.json';
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2)); const tempFile = collectionFilePath + '.tmp';
fs.renameSync(tempFile, this.dbFile); fs.writeFileSync(tempFile, JSON.stringify(collectionData, null, 2));
fs.renameSync(tempFile, collectionFilePath);
} }
return true; return true;
} catch (err) { } 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(); 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 { NodeConfigComponent } from './shared/components/node-config/node-config.component';
import { LNPConfigComponent } from './shared/components/node-config/lnp-config/lnp-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 { 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 { 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 { 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'; 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 { ErrorComponent } from './shared/components/error/error.component';
import { AuthGuard } from './shared/services/auth.guard'; import { AuthGuard } from './shared/services/auth.guard';
import { ExperimentalSettingsComponent } from './shared/components/node-config/experimental-settings/experimental-settings.component'; 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 = [ export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'login' }, { path: '', pathMatch: 'full', redirectTo: 'login' },
@ -42,14 +37,14 @@ export const routes: Routes = [
}, },
{ {
path: 'config', component: NodeConfigComponent, canActivate: [AuthGuard], children: [ path: 'config', component: NodeConfigComponent, canActivate: [AuthGuard], children: [
{ path: '', pathMatch: 'full', redirectTo: 'layout' }, { path: '', pathMatch: 'full', redirectTo: 'applayout' },
{ path: 'layout', component: NodeSettingsComponent, canActivate: [AuthGuard] }, { path: 'applayout', component: NodeSettingsComponent, canActivate: [AuthGuard] },
{ path: 'pglayout', component: PageSettingsComponent, canActivate: [AuthGuard] },
{ {
path: 'services', component: ServicesSettingsComponent, canActivate: [AuthGuard], children: [ path: 'services', component: ServicesSettingsComponent, canActivate: [AuthGuard], children: [
{ path: '', pathMatch: 'full', redirectTo: 'loop' }, { path: '', pathMatch: 'full', redirectTo: 'loop' },
{ path: 'loop', component: LoopServiceSettingsComponent, canActivate: [AuthGuard] }, { path: 'loop', component: LoopServiceSettingsComponent, canActivate: [AuthGuard] },
{ path: 'boltz', component: BoltzServiceSettingsComponent, canActivate: [AuthGuard] }, { path: 'boltz', component: BoltzServiceSettingsComponent, canActivate: [AuthGuard] }
{ path: 'peerswap', component: PeerswapServiceSettingsComponent, canActivate: [AuthGuard] }
] ]
}, },
{ path: 'experimental', component: ExperimentalSettingsComponent, 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', pathMatch: 'full', redirectTo: 'loop/loopout' },
{ path: 'loop/:selTab', component: LoopComponent }, { path: 'loop/:selTab', component: LoopComponent },
{ path: 'boltz', pathMatch: 'full', redirectTo: 'boltz/swapout' }, { path: 'boltz', pathMatch: 'full', redirectTo: 'boltz/swapout' },
{ path: 'boltz/:selTab', component: BoltzRootComponent }, { 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: 'help', component: HelpComponent }, { 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, { 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> </div>
<mat-divider [inset]="true"></mat-divider> <mat-divider [inset]="true"></mat-divider>
<div fxLayout="column" fxFlex="20" class="my-1"> <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> <span class="foreground-secondary-text">{{lookupResult[0]?.short_channel_id}}</span>
</div> </div>
<mat-divider [inset]="true"></mat-divider> <mat-divider [inset]="true"></mat-divider>
@ -89,7 +89,7 @@
</div> </div>
<mat-divider [inset]="true"></mat-divider> <mat-divider [inset]="true"></mat-divider>
<div fxLayout="column" fxFlex="20" class="my-1"> <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> <span class="foreground-secondary-text">{{lookupResult[1]?.short_channel_id}}</span>
</div> </div>
<mat-divider [inset]="true"></mat-divider> <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 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> <button mat-flat-button color="primary" tabindex="4" type="submit" (click)="onLookup()">Lookup</button>
</div> </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 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"> <div fxLayout="row" fxFlex="100" fxLayoutAlign="start center">
<span class="page-title font-bold-500">{{lookupFields[selectedFieldId].name}} Details</span> <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" *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> <span fxFlex="100" *ngSwitchDefault><h3>Error! Unable to find details!</h3></span>
</div> </div>
</div> </div>
</mat-card-content> </mat-card-content>
</div> </div>
</div> </div>

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

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

@ -1,5 +1,5 @@
<div fxLayout="column" *ngIf="lookupResult" class="mt-1"> <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 fxLayout="row">
<div fxFlex="30"> <div fxFlex="30">
<h4 fxLayoutAlign="start" class="font-bold-500">Alias</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Alias</h4>
@ -28,21 +28,23 @@
<table mat-table #table [dataSource]="addresses" matSort class="overflow-auto"> <table mat-table #table [dataSource]="addresses" matSort class="overflow-auto">
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th> <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>
<ng-container matColumnDef="address"> <ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Address</th> <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>
<ng-container matColumnDef="port"> <ng-container matColumnDef="port">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Port</th> <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>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="pl-1"><span fxLayoutAlign="end center">Actions</span></th> <th mat-header-cell *matHeaderCellDef>
<td mat-cell *matCellDef="let address" class="pl-1"> <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"> <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> </span>
</td> </td>
</ng-container> </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; @ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@Input() lookupResult: LookupNode; @Input() lookupResult: LookupNode;
public featureDescriptions: string[] = []; public featureDescriptions: string[] = [];
public addresses: any; public addresses: any = new MatTableDataSource([]);
public displayedColumns = ['type', 'address', 'port', 'actions']; public displayedColumns = ['type', 'address', 'port', 'actions'];
constructor(private logger: LoggerService, private snackBar: MatSnackBar) { } constructor(private logger: LoggerService, private snackBar: MatSnackBar) { }

@ -27,42 +27,46 @@
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar> <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}"> <table mat-table #table [dataSource]="qrHops" matSort [ngClass]="{'overflow-auto error-border': flgLoading[0]==='error','overflow-auto': true}">
<ng-container matColumnDef="id"> <ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th> <th mat-header-cell *matHeaderCellDef mat-sort-header>ID</th>
<td mat-cell *matCellDef="let hop"> {{hop?.id}} </td> <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>
<ng-container matColumnDef="alias"> <ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Alias</th>
<td mat-cell *matCellDef="let hop"> {{hop?.alias}} </td> <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>
<ng-container matColumnDef="channel"> <ng-container matColumnDef="channel">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Channel </th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Channel</th>
<td mat-cell *matCellDef="let hop"> {{hop?.channel}} </td> <td mat-cell *matCellDef="let hop">{{hop?.channel}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="direction"> <ng-container matColumnDef="direction">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Direction </th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Direction</th>
<td mat-cell *matCellDef="let hop"> {{hop?.direction}} </td> <td mat-cell *matCellDef="let hop">{{hop?.direction}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="delay"> <ng-container matColumnDef="delay">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Delay </th> <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 mat-cell *matCellDef="let hop"><span fxLayoutAlign="end center">{{hop?.delay | number}} </span></td>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="msatoshi"> <ng-container matColumnDef="msatoshi">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Amount (Sats) </th> <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}} <td mat-cell *matCellDef="let hop"><span fxLayoutAlign="end center">{{hop?.msatoshi/1000 | number}}</span></td>
</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>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="pl-4 pr-3"><span fxLayoutAlign="end center">Actions</span></th> <th mat-header-cell *matHeaderCellDef>
<td mat-cell *matCellDef="let hop" class="pl-4"> <div class="bordered-box table-actions-select" fxLayoutAlign="center center">Actions</div>
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onHopClick(hop, $event)">View Info</button> </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> </td>
</ng-container> </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> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
</div> </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 { faRoute, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { Routes } from '../../../shared/models/clnModels'; 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 { CommonService } from '../../../shared/services/common.service';
import { CLNEffects } from '../../store/cln.effects'; import { CLNEffects } from '../../store/cln.effects';
import { RTLState } from '../../../store/rtl.state'; import { RTLState } from '../../../store/rtl.state';
import { openAlert } from '../../../store/rtl.actions'; import { openAlert } from '../../../store/rtl.actions';
import { getQueryRoutes } from '../../store/cln.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({ @Component({
selector: 'rtl-cln-query-routes', selector: 'rtl-cln-query-routes',
@ -24,38 +27,35 @@ export class CLNQueryRoutesComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined; @ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild('queryRoutesForm', { static: true }) form: any; @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 destinationPubkey = '';
public amount: number | null = null; public amount: number | null = null;
public qrHops: any; public qrHops: any = new MatTableDataSource([]);
public flgSticky = false;
public displayedColumns: any[] = []; public displayedColumns: any[] = [];
public flgLoading: Array<Boolean | 'error'> = [false]; // 0: peers public flgLoading: Array<Boolean | 'error'> = [false]; // 0: peers
public faRoute = faRoute; public faRoute = faRoute;
public faExclamationTriangle = faExclamationTriangle; public faExclamationTriangle = faExclamationTriangle;
public screenSize = ''; public screenSize = '';
public screenSizeEnum = ScreenSizeEnum; 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) { constructor(private store: Store<RTLState>, private clnEffects: CLNEffects, private commonService: CommonService) {
this.screenSize = this.commonService.getScreenSize(); 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() { 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.clnEffects.setQueryRoutesCL.pipe(takeUntil(this.unSubs[1])).subscribe((queryRoute) => {
this.qrHops = new MatTableDataSource([]);
this.qrHops.data = []; this.qrHops.data = [];
if (queryRoute.routes && queryRoute.routes.length && queryRoute.routes.length > 0) { if (queryRoute.routes && queryRoute.routes.length && queryRoute.routes.length > 0) {
this.flgLoading[0] = false; this.flgLoading[0] = false;
@ -66,6 +66,7 @@ export class CLNQueryRoutesComponent implements OnInit, OnDestroy {
} }
this.qrHops.sort = this.sort; 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.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> </button>
<mat-menu #menuTransactions="matMenu" class="dashboard-vert-menu" xPosition="before"> <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> <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> </ng-template>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</span> </span>
<h3 *ngSwitchDefault>Error! Unable to find information!</h3> <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"> <form fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-sm="row wrap" #formAsk="ngForm">
<div fxFlex="100" fxLayout="row" class="alert alert-warn"> <div fxFlex="100" fxLayout="row" class="alert alert-warn">
<fa-icon [icon]="faExclamationTriangle" class="mr-1 alert-icon"></fa-icon> <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>
<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 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"> <div fxFlex="30">
@ -23,8 +23,8 @@
<mat-error *ngIf="!channelAmount">Channel amount is required.</mat-error> <mat-error *ngIf="!channelAmount">Channel amount is required.</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex="34"> <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> <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="!channelOpeningFeeRate">Channel opening fee rate is required.</mat-error> <mat-error *ngIf="!channel_opening_feeRate">Channel opening fee rate is required.</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
</form> </form>
@ -38,11 +38,11 @@
</span> </span>
</div> </div>
<mat-form-field fxFlex="34"> <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"> <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="!nodeCapacity">Node capacity is required.</mat-error> <mat-error *ngIf="!node_capacity">Node capacity is required.</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex="34"> <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> </mat-form-field>
</div> </div>
</form> --> </form> -->
@ -51,62 +51,95 @@
<fa-icon [icon]="faUsers" class="page-title-img mr-1"></fa-icon> <fa-icon [icon]="faUsers" class="page-title-img mr-1"></fa-icon>
<span class="page-title">Liquidity Providing Peers</span> <span class="page-title">Liquidity Providing Peers</span>
</div> </div>
<mat-form-field fxFlex="30"> <div fxFlex="30" fxLayoutAlign.gt-xs="space-between center" fxLayout="row" fxLayoutAlign="space-between stretch">
<div fxLayout="row" fxLayoutAlign="start start"> <mat-form-field fxFlex="49">
<input matInput (keyup)="applyFilter()" [(ngModel)]="selFilter" placeholder="Filter"> <mat-select placeholder="Filter By" tabindex="1" [(ngModel)]="selFilterBy" (selectionChange)="selFilter=''; applyFilter()" name="filterBy">
</div> <mat-option *ngFor="let column of ['all'].concat(displayedColumns.slice(0, -1))" [value]="column">{{getLabel(column)}}</mat-option>
</mat-form-field> </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>
<div [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container"> <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> <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}"> <table mat-table #table [dataSource]="liquidityNodes" matSort [ngClass]="{'overflow-auto error-border': errorMessage !== '','overflow-auto': true}">
<ng-container matColumnDef="alias"> <ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th> <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"> <td mat-cell *matCellDef="let lqNode">
{{lqNode?.alias}} <div class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<mat-chip-list class="ml-half" aria-label="Address Types"> <span fxLayout="row" fxLayoutAlign="start center" class="ellipsis-child">
<mat-chip *ngFor="let addrType of lqNode.address_types" color="primary" selected> {{lqNode?.alias}}
{{addrType === 'tor' ? 'Tor' : addrType === 'ipv' ? 'Clearnet' : addrType}} <mat-chip-list class="ml-half" aria-label="Address Types">
</mat-chip> <mat-chip *ngFor="let addrType of lqNode.address_types" color="primary" selected>
</mat-chip-list> {{addrType === 'tor' ? 'Tor' : addrType === 'ipv' ? 'Clearnet' : addrType}}
</mat-chip>
</mat-chip-list>
</span>
</div>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="capacityChannels"> <ng-container matColumnDef="nodeid">
<th mat-header-cell *matHeaderCellDef> Capacity/Channels </th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Node ID</th>
<td mat-cell *matCellDef="let lqNode"> <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> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="leaseFee"> <ng-container matColumnDef="last_timestamp">
<th mat-header-cell *matHeaderCellDef> Lease Fee </th> <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"> <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'}}% {{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> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="routingFee"> <ng-container matColumnDef="routing_fee">
<th mat-header-cell *matHeaderCellDef> Routing Fee </th> <th mat-header-cell *matHeaderCellDef> Routing Fee</th>
<td mat-cell *matCellDef="let lqNode"> <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 {{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> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="channelOpeningFee"> <ng-container matColumnDef="channel_opening_fee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Channel Opening Fee </th> <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"> <td mat-cell *matCellDef="let lqNode">
<span fxLayoutAlign="end center"> <span fxLayoutAlign="end center">
{{lqNode.channelOpeningFee | number:'1.0-0'}} Sats {{lqNode?.option_will_fund?.funding_weight | number:'1.0-0'}}
</span> </span>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3"> <th mat-header-cell *matHeaderCellDef>
<div class="bordered-box table-actions-select"> <div class="bordered-box table-actions-select" fxLayoutAlign="center center">
<mat-select placeholder="Actions" tabindex="1" class="mr-0"> <mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger> <mat-select-trigger></mat-select-trigger>
<mat-option (click)="onDownloadCSV()">Download CSV</mat-option> <mat-option (click)="onDownloadCSV()">Download CSV</mat-option>
</mat-select> </mat-select>
</div> </div>
</th> </th>
<td mat-cell *matCellDef="let lqNode" fxLayoutAlign="end center" class="px-3"> <td mat-cell *matCellDef="let lqNode" fxLayoutAlign="end center">
<div class="bordered-box table-actions-select" fxLayoutAlign="center center"> <div class="bordered-box table-actions-select" fxLayoutAlign="center center">
<mat-select placeholder="Actions" tabindex="1" class="mr-0"> <mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger> <mat-select-trigger></mat-select-trigger>
@ -126,7 +159,7 @@
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_lqNode']" [ngClass]="{'display-none': liquidityNodes?.data && liquidityNodes?.data?.length>0}"></tr> <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> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
</div> </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 { DataService } from '../../../shared/services/data.service';
import { LoggerService } from '../../../shared/services/logger.service'; import { LoggerService } from '../../../shared/services/logger.service';
import { CommonService } from '../../../shared/services/common.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 { GetInfo, LookupNode } from '../../../shared/models/clnModels';
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload'; import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
import { openAlert, openConfirmation } from '../../../store/rtl.actions'; 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 { RTLState } from '../../../store/rtl.state';
import { RTLEffects } from '../../../store/rtl.effects'; import { RTLEffects } from '../../../store/rtl.effects';
import { CLNOpenLiquidityChannelComponent } from '../open-liquidity-channel-modal/open-liquidity-channel-modal.component'; import { CLNOpenLiquidityChannelComponent } from '../open-liquidity-channel-modal/open-liquidity-channel-modal.component';
import { nodeInfoAndNodeSettingsAndBalance } from '../../store/cln.selector'; import { clnPageSettings, nodeInfoAndNodeSettingsAndBalance } from '../../store/cln.selector';
import { DecimalPipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { ColumnDefinition, PageSettings, TableSetting } from '../../../shared/models/pageSettings';
import { CamelCaseWithReplacePipe } from '../../../shared/pipes/app.pipe';
@Component({ @Component({
selector: 'rtl-cln-liquidity-ads-list', selector: 'rtl-cln-liquidity-ads-list',
@ -33,6 +35,11 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined; @ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | 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 askTooltipMsg = '';
public nodesTooltipMsg = ''; public nodesTooltipMsg = '';
public displayedColumns: any[] = []; public displayedColumns: any[] = [];
@ -42,12 +49,11 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
public totalBalance = 0; public totalBalance = 0;
public information: GetInfo; public information: GetInfo;
public channelAmount = 100000; public channelAmount = 100000;
public channelOpeningFeeRate = 10; public channel_opening_feeRate = 10;
public nodeCapacity = 500000; public node_capacity = 500000;
public channelCount = 5; public channel_count = 5;
public liquidityNodesData: LookupNode[] = []; public liquidityNodesData: LookupNode[] = [];
public liquidityNodes: any; public liquidityNodes: any = new MatTableDataSource([]);
public flgSticky = false;
public pageSize = PAGE_SIZE; public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS; public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = ''; public screenSize = '';
@ -56,30 +62,37 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
public selFilter = ''; public selFilter = '';
public apiCallStatus: ApiCallStatusPayload = { status: APICallStatusEnum.INITIATED }; public apiCallStatus: ApiCallStatusPayload = { status: APICallStatusEnum.INITIATED };
public apiCallStatusEnum = APICallStatusEnum; public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), 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.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 = '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(); 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 { 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({ subscribe({
next: ([infoSettingsBalSelector, nodeListRes]) => { next: ([infoSettingsBalSelector, nodeListRes]) => {
this.information = infoSettingsBalSelector.information; this.information = infoSettingsBalSelector.information;
@ -111,7 +124,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
onCalculateOpeningFee() { onCalculateOpeningFee() {
this.liquidityNodesData.forEach((lqNode) => { this.liquidityNodesData.forEach((lqNode) => {
if (lqNode.option_will_fund) { 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(); } if (this.paginator) { this.paginator.firstPage(); }
@ -125,21 +138,59 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
this.liquidityNodes.filter = this.selFilter.trim().toLowerCase(); 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[]) { loadLiqNodesTable(liqNodes: LookupNode[]) {
this.liquidityNodes = new MatTableDataSource<LookupNode>([...liqNodes]); 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.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.sort = this.sort;
this.liquidityNodes.paginator = this.paginator; this.liquidityNodes.sort?.sort({ id: this.tableSetting.sortBy, start: this.tableSetting.sortOrder, disableClear: true });
if (this.sort) { this.sort.sort({ id: 'channelOpeningFee', start: 'asc', disableClear: true }); } this.setFilterPredicate();
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.applyFilter(); 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(); // this.onFilter();
} }
@ -156,7 +207,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
node: lqNode, node: lqNode,
balance: this.totalBalance, balance: this.totalBalance,
requestedAmount: this.channelAmount, requestedAmount: this.channelAmount,
feeRate: this.channelOpeningFeeRate, feeRate: this.channel_opening_feeRate,
localAmount: 20000 localAmount: 20000
}; };
this.store.dispatch(openAlert({ 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) { if (confirmRes) {
this.onOpenChannel(lqNode); this.onOpenChannel(lqNode);
} }
@ -222,8 +273,8 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
} }
onFilterReset() { onFilterReset() {
this.nodeCapacity = 0; this.node_capacity = 0;
this.channelCount = 0; this.channel_count = 0;
} }
ngOnDestroy() { ngOnDestroy() {

@ -30,7 +30,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div fxFlex="100" class="alert alert-info mt-4"> <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>
<div fxFlex="100" class="alert alert-danger mt-2" *ngIf="channelConnectionError !== ''"> <div fxFlex="100" class="alert alert-danger mt-2" *ngIf="channelConnectionError !== ''">
<fa-icon [icon]="faExclamationTriangle" class="mr-1 alert-icon"></fa-icon> <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"> <table mat-table #table [dataSource]="node.addresses" matSort class="overflow-auto">
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th> <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>
<ng-container matColumnDef="address"> <ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Address</th> <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>
<ng-container matColumnDef="port"> <ng-container matColumnDef="port">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Port</th> <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>
<tr mat-header-row *matHeaderRowDef="displayedColumns;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

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

@ -68,7 +68,7 @@ export class CLNOpenLiquidityChannelComponent implements OnInit, OnDestroy {
} }
calculateFee() { 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 { onOpenChannel(): boolean | void {

@ -15,7 +15,7 @@
<mat-form-field fxFlex="30"> <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"> <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> <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-error *ngIf="!transaction.satoshis">{{amountError}}</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex="10" fxLayoutAlign="start end"> <mat-form-field fxFlex="10" fxLayoutAlign="start end">

@ -77,7 +77,17 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
public screenSizeEnum = ScreenSizeEnum; public screenSizeEnum = ScreenSizeEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
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(); 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; this.transaction.minconf = this.sendFundFormGroup.controls.flgMinConf.value ? this.sendFundFormGroup.controls.minConfValue.value : null;
} else { } else {
delete this.transaction.minconf; 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; delete this.transaction.utxos;
this.store.dispatch(setChannelTransaction({ payload: this.transaction })); this.store.dispatch(setChannelTransaction({ payload: this.transaction }));

@ -4,13 +4,13 @@
<ng-template mat-tab-label> <ng-template mat-tab-label>
<span matBadge="{{numUtxos}}" matBadgeOverlap="false" class="tab-badge">UTXOs</span> <span matBadge="{{numUtxos}}" matBadgeOverlap="false" class="tab-badge">UTXOs</span>
</ng-template> </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>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<span matBadge="{{numDustUtxos}}" matBadgeOverlap="false" class="tab-badge">Dust UTXOs</span> <span matBadge="{{numDustUtxos}}" matBadgeOverlap="false" class="tab-badge">Dust UTXOs</span>
</ng-template> </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>
</mat-tab-group> </mat-tab-group>
</div> </div>

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

@ -19,10 +19,9 @@ export class CLNUTXOTablesComponent implements OnInit, OnDestroy {
@Input() selectedTableIndex = 0; @Input() selectedTableIndex = 0;
@Output() readonly selectedTableIndexChange = new EventEmitter<number>(); @Output() readonly selectedTableIndexChange = new EventEmitter<number>();
public utxos: UTXO[] = [];
public numUtxos = 0; public numUtxos = 0;
public dustUtxos: UTXO[] = [];
public numDustUtxos = 0; public numDustUtxos = 0;
public DUST_AMOUNT = 1000;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<RTLState>) { } 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])). this.store.select(utxos).pipe(takeUntil(this.unSubs[0])).
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => { subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
if (utxosSeletor.utxos && utxosSeletor.utxos.length > 0) { if (utxosSeletor.utxos && utxosSeletor.utxos.length > 0) {
this.utxos = utxosSeletor.utxos; this.numUtxos = utxosSeletor.utxos.length || 0;
this.numUtxos = this.utxos.length; this.numDustUtxos = utxosSeletor.utxos?.filter((utxo) => +(utxo.value || 0) < this.DUST_AMOUNT).length || 0;
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.logger.info(utxosSeletor); 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" fxLayoutAlign="start stretch" fxFlex="100" 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 fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70"></div> <div fxFlex="70" fxLayoutAlign="start start" fxLayoutAlign.gt-sm="start center"></div>
<mat-form-field fxFlex="30"> <div fxFlex="30" fxLayoutAlign.gt-xs="space-between center" fxLayout="row" fxLayoutAlign="space-between stretch">
<input matInput (keyup)="applyFilter()" [(ngModel)]="selFilter" placeholder="Filter"> <mat-form-field fxFlex="49">
</mat-form-field> <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>
<div fxLayout="row" fxLayoutAlign="start start"> <div fxLayout="row" fxLayoutAlign="start start">
<div [perfectScrollbar] class="table-container" fxFlex="100"> <div [perfectScrollbar] class="table-container" fxFlex="100">
<mat-progress-bar *ngIf="apiCallStatus?.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar> <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}"> <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"> <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"> <td mat-cell *matCellDef="let utxo">
<span fxLayout="row" class="ellipsis-parent" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '50rem'}"> <span fxLayout="row" class="ellipsis-parent" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '10rem' : colWidth}">
<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 class="ellipsis-child">{{utxo.txid}}</span> <span class="ellipsis-child">{{utxo.txid}}</span>
</span> </span>
</td> </td>
</ng-container> </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"> <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"> <td mat-cell *matCellDef="let utxo"><span fxLayoutAlign="end center">
{{utxo?.output | number}} </span></td> {{utxo?.output | number}} </span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <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"> <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" *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> <span fxLayoutAlign="end center" class="red" *ngIf="utxo.value < 0">({{utxo.value * -1 | number}})</span>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="blockheight"> <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"> <td mat-cell *matCellDef="let utxo"><span fxLayoutAlign="end center">
{{utxo?.blockheight | number}} </span></td> {{utxo?.blockheight | number}} </span></td>
</ng-container> </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"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3"> <th mat-header-cell *matHeaderCellDef>
<div class="bordered-box table-actions-select"> <div class="bordered-box table-actions-select" fxLayoutAlign="center center">
<mat-select placeholder="Actions" tabindex="1" class="mr-0"> <mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger> <mat-select-trigger></mat-select-trigger>
<mat-option (click)="onDownloadCSV()">Download CSV</mat-option> <mat-option (click)="onDownloadCSV()">Download CSV</mat-option>
</mat-select> </mat-select>
</div> </div>
</th> </th>
<td mat-cell *matCellDef="let utxo" class="pl-3" fxLayoutAlign="end center"> <td mat-cell *matCellDef="let utxo" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onUTXOClick(utxo, $event)">View Info</button> <button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onUTXOClick(utxo, $event)" class="table-actions-button">View Info</button>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="no_utxo"> <ng-container matColumnDef="no_utxo">
@ -62,7 +99,7 @@
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_utxo']" [ngClass]="{'display-none': listUTXOs?.data && listUTXOs?.data?.length>0}"></tr> <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> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
<mat-paginator [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" [showFirstLastButtons]="screenSize === screenSizeEnum.XS ? false : true" class="mb-1"></mat-paginator> <mat-paginator [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" [showFirstLastButtons]="screenSize === screenSizeEnum.XS ? false : true" class="mb-1"></mat-paginator>

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

@ -1,5 +1,6 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule } from '@ngrx/store'; import { StoreModule } from '@ngrx/store';
import { CommonService } from '../../../../shared/services/common.service'; import { CommonService } from '../../../../shared/services/common.service';
import { DataService } from '../../../../shared/services/data.service'; import { DataService } from '../../../../shared/services/data.service';
@ -23,6 +24,7 @@ describe('CLNOnChainUtxosComponent', () => {
imports: [ imports: [
BrowserAnimationsModule, BrowserAnimationsModule,
SharedModule, SharedModule,
RouterTestingModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer }) StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer })
], ],
providers: [ 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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -7,14 +8,16 @@ import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { UTXO } from '../../../../shared/models/clnModels'; 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 { ApiCallStatusPayload } from '../../../../shared/models/apiCallsPayload';
import { LoggerService } from '../../../../shared/services/logger.service'; import { LoggerService } from '../../../../shared/services/logger.service';
import { CommonService } from '../../../../shared/services/common.service'; import { CommonService } from '../../../../shared/services/common.service';
import { RTLState } from '../../../../store/rtl.state'; import { RTLState } from '../../../../store/rtl.state';
import { openAlert } from '../../../../store/rtl.actions'; 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({ @Component({
selector: 'rtl-cln-on-chain-utxos', selector: 'rtl-cln-on-chain-utxos',
@ -24,16 +27,22 @@ import { utxos } from '../../../store/cln.selector';
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('UTXOs') } { 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(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | undefined; @ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | undefined;
@Input() numDustUTXOs = 0; @Input() numDustUTXOs = 0;
@Input() isDustUTXO = false; @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 displayedColumns: any[] = [];
public listUTXOs: any; public utxos: UTXO[];
public flgSticky = false; public dustUtxos: UTXO[];
public listUTXOs: any = new MatTableDataSource([]);
public pageSize = PAGE_SIZE; public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS; public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = ''; public screenSize = '';
@ -42,53 +51,66 @@ export class CLNOnChainUtxosComponent implements OnInit, OnChanges, AfterViewIni
public selFilter = ''; public selFilter = '';
public apiCallStatus: ApiCallStatusPayload | null = null; public apiCallStatus: ApiCallStatusPayload | null = null;
public apiCallStatusEnum = APICallStatusEnum; 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(); 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() { ngOnInit() {
this.store.select(utxos).pipe(takeUntil(this.unSubs[0])). this.router.routeReuseStrategy.shouldReuseRoute = () => false;
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => { 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.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) { 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.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() { 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); 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) { onUTXOClick(selUtxo: UTXO, event: any) {
const reorderedUTXO = [ const reorderedUTXO = [
[{ key: 'txid', value: selUtxo.txid, title: 'Transaction ID', width: 100 }], [{ 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[]) { loadUTXOsTable(utxos: any[]) {
this.listUTXOs = new MatTableDataSource<UTXO>([...utxos]); 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.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.listUTXOs.paginator = this.paginator;
this.setFilterPredicate();
this.applyFilter(); this.applyFilter();
this.logger.info(this.listUTXOs); this.logger.info(this.listUTXOs);
} }

@ -75,7 +75,7 @@
<div *ngIf="showAdvanced"> <div *ngIf="showAdvanced">
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="100"> <div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Funding Transaction Id</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Funding Transaction ID</h4>
<span class="foreground-secondary-text">{{channel.funding_txid}}</span> <span class="foreground-secondary-text">{{channel.funding_txid}}</span>
</div> </div>
</div> </div>

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

Loading…
Cancel
Save