Release 0.9.1 (#513)

ECL Enable MPP #452
Authentication layout fix on Safari #473
Updated error message for download backup #471
Increase badge width with number #453
CLT Sweep All #451
CLT Invoice incorrect fiat conversion #454
UTXO Hint, MPP Indentation, ECL Inactive Peer Reconnect #459, #460, #462
LND filter outgoing channel for send payment #463
Show Description on Payments Info #467
Added Description in Payments Info #489
ECL Added Settled Amount in Invoices list #441
List the UTXOs for the wallet #420
Loop upgraded to 9.0 with backward compatibility #472
Confirmation message updated for force close
Display the active HTLCs #498
Path information missing for transaction #497
Problem parsing eclair.conf #383
Created Contributing.md
Separate fee and payments calls #428
Updated Forwarding History Table and View Info #437 (#507)
Showing channel ID when alias is missing #495 & #481
Add payment description #510
Force close pending channels #447
pull/518/head v0.9.1
ShahanaFarooqui 4 years ago committed by GitHub
parent bf8c687d6d
commit 0ec41f2de1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,7 +4,7 @@
<a href="https://snyk.io/test/github/Ride-The-Lightning/RTL"><img src="https://snyk.io/test/github/Ride-The-Lightning/RTL/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/Ride-The-Lightning/RTL" style="max-width:100%;"></a> <a href="https://snyk.io/test/github/Ride-The-Lightning/RTL"><img src="https://snyk.io/test/github/Ride-The-Lightning/RTL/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/Ride-The-Lightning/RTL" style="max-width:100%;"></a>
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE) [![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE)
**Intro** -- [Application Features](docs/Application_features.md) -- [Road Map](docs/Roadmap.md) -- [Application Configurations](docs/Application_configurations) -- [C-Lightning](docs/C-Lightning-setup.md) -- [Eclair](docs/Eclair-setup.md) **Intro** -- [Application Features](docs/Application_features.md) -- [Road Map](docs/Roadmap.md) -- [Application Configurations](docs/Application_configurations) -- [C-Lightning](docs/C-Lightning-setup.md) -- [Eclair](docs/Eclair-setup.md) -- [Contribution](docs/Contributing.md)
* [Introduction](#intro) * [Introduction](#intro)
* [Architecture](#arch) * [Architecture](#arch)

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

@ -12,8 +12,8 @@
<link rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="styles.e251e2de33649e5868e8.css"></head> <link rel="stylesheet" href="styles.a87bd00d80a3f00717e3.css"></head>
<body> <body>
<rtl-app></rtl-app> <rtl-app></rtl-app>
<script src="runtime.9543702dbddde12f9bb5.js" defer></script><script src="polyfills-es5.2ac0d98b22574ae745b1.js" nomodule defer></script><script src="polyfills.5ae721a6ae5ab597a53d.js" defer></script><script src="main.f38d9b0b63f350c3adfc.js" defer></script></body> <script src="runtime.847357e57be0d1216bef.js" defer></script><script src="polyfills-es5.2ac0d98b22574ae745b1.js" nomodule defer></script><script src="polyfills.5ae721a6ae5ab597a53d.js" defer></script><script src="main.b4fa9dae26f218a7142d.js" defer></script></body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],f=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(l&&l(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"9bb271dd8dffd2d994a5",6:"53681db8293fc8ee832d",7:"4a00e92294df28ac9ca1",8:"978bb00e2e8f4e0e9114"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(f);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var f=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var f=0;f<i.length;f++)r(i[f]);var l=c;t()}([]); !function(e){function r(r){for(var n,a,i=r[0],c=r[1],f=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(l&&l(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"07193c762dd7469c63c8",6:"a09d4a670f5611bfdc5c",7:"e955a16c6af1870f3a25",8:"46ac257a195b700be065"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(f);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var f=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var f=0;f<i.length;f++)r(i[f]);var l=c;t()}([]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,7 +1,6 @@
var fs = require('fs'); var fs = require('fs');
var crypto = require('crypto'); var crypto = require('crypto');
var path = require('path'); var path = require('path');
var ini = require('ini');
var common = {}; var common = {};
common.rtl_conf_file_path = ''; common.rtl_conf_file_path = '';
@ -17,8 +16,12 @@ common.secret_key = crypto.randomBytes(64).toString('hex');
common.nodes = []; common.nodes = [];
common.selectedNode = {}; common.selectedNode = {};
common.getSelSwapServerUrl = () => { common.getSwapServerOptions = () => {
return common.selectedNode.swap_server_url; return {
url: common.selectedNode.swap_server_url,
rejectUnauthorized: false,
json: true
};
}; };
common.getSelLNServerUrl = () => { common.getSelLNServerUrl = () => {

@ -10,6 +10,7 @@ var connect = {};
var errMsg = ''; var errMsg = '';
var request = require('request'); var request = require('request');
var ini = require('ini'); var ini = require('ini');
var parseHocon = require('hocon-parser');
common.path_separator = (platform === 'win32') ? '\\' : '/'; common.path_separator = (platform === 'win32') ? '\\' : '/';
connect.setDefaultConfig = () => { connect.setDefaultConfig = () => {
@ -150,7 +151,9 @@ connect.validateNodeConfig = (config) => {
let exists = fs.existsSync(common.nodes[idx].config_path); let exists = fs.existsSync(common.nodes[idx].config_path);
if (exists) { if (exists) {
try { try {
common.nodes[idx].ln_api_password = ini.parse(fs.readFileSync(common.nodes[idx].config_path, 'utf-8'))['eclair.api.password']; let configFile = fs.readFileSync(common.nodes[idx].config_path, 'utf-8');
let iniParsed = ini.parse(configFile);
common.nodes[idx].ln_api_password = iniParsed['eclair.api.password'] ? iniParsed['eclair.api.password'] : parseHocon(configFile).eclair.api.password;
} catch (err) { } catch (err) {
errMsg = errMsg + '\nSomething went wrong while reading config file: \n' + err; errMsg = errMsg + '\nSomething went wrong while reading config file: \n' + err;
} }

@ -204,7 +204,10 @@ exports.getFile = (req, res, next) => {
logger.info({fileName: 'Conf', msg: 'Channel Point: ' + req.query.channel + ', File Path: ' + file}); logger.info({fileName: 'Conf', msg: 'Channel Point: ' + req.query.channel + ', File Path: ' + file});
fs.readFile(file, 'utf8', function(err, data) { fs.readFile(file, 'utf8', function(err, data) {
if (err) { if (err) {
logger.error({fileName: 'Conf', lineNum: 207, msg: 'Reading File Failed!'}); logger.error({fileName: 'Conf', lineNum: 207, msg: 'Reading File Failed!' + JSON.stringify(err)});
if (err.code && err.code === 'ENOENT') {
err.code = 'Backup File Not Found!';
}
res.status(500).json({ res.status(500).json({
message: "Reading File Failed!", message: "Reading File Failed!",
error: err error: err

@ -100,8 +100,9 @@ exports.setChannelFee = (req, res, next) => {
} }
exports.closeChannel = (req, res, next) => { exports.closeChannel = (req, res, next) => {
req.setTimeout(60000 * 10); // timeout 10 mins
options = common.getOptions(); options = common.getOptions();
const unilateralTimeoutQuery = req.query.unilateralTimeout ? '?unilateralTimeout=' + req.query.unilateralTimeout : ''; const unilateralTimeoutQuery = req.query.force ? '?unilateralTimeout=1' : '';
options.url = common.getSelLNServerUrl() + '/v1/channel/closeChannel/' + req.params.channelId + unilateralTimeoutQuery; options.url = common.getSelLNServerUrl() + '/v1/channel/closeChannel/' + req.params.channelId + unilateralTimeoutQuery;
logger.info({fileName: 'Channels', msg: 'Closing Channel: ' + options.url}); logger.info({fileName: 'Channels', msg: 'Closing Channel: ' + options.url});
request.delete(options).then((body) => { request.delete(options).then((body) => {

@ -202,6 +202,7 @@ exports.closeChannel = (req, res, next) => {
options.url = common.getSelLNServerUrl() + '/forceclose'; options.url = common.getSelLNServerUrl() + '/forceclose';
} }
options.form = { channelId: req.query.channelId }; options.form = { channelId: req.query.channelId };
logger.info({fileName: 'Channels', msg: 'Close Channel URL: ' + JSON.stringify(options.url)});
logger.info({fileName: 'Channels', msg: 'Close Channel Params: ' + JSON.stringify(options.form)}); logger.info({fileName: 'Channels', msg: 'Close Channel Params: ' + JSON.stringify(options.form)});
request.post(options).then((body) => { request.post(options).then((body) => {
logger.info({fileName: 'Channels', msg: 'Close Channel Response: ' + JSON.stringify(body)}); logger.info({fileName: 'Channels', msg: 'Close Channel Response: ' + JSON.stringify(body)});

@ -13,21 +13,60 @@ exports.getFees = (req, res, next) => {
to: tillToday to: tillToday
}; };
request.post(options).then((body) => { request.post(options).then((body) => {
logger.info({fileName: 'Fees', msg: 'Audit Response: ' + JSON.stringify(body)}); logger.info({fileName: 'Fees', msg: 'Fee Response: ' + JSON.stringify(body)});
let resBody = { let fees = { daily_fee: 0, daily_txs: 0, weekly_fee: 0, weekly_txs: 0, monthly_fee: 0, monthly_txs: 0 };
fees: {daily_fee: 0, daily_txs: 0, weekly_fee: 0, weekly_txs: 0, monthly_fee: 0, monthly_txs: 0 },
payments: {
sent: body && body.sent ? body.sent : [],
received: body && body.received ? body.received : [],
relayed: body && body.relayed ? body.relayed : []
}
};
let current_time = Math.round((new Date().getTime())); let current_time = Math.round((new Date().getTime()));
let month_start_time = current_time - 2629743000; let month_start_time = current_time - 2629743000;
let week_start_time = current_time - 604800000; let week_start_time = current_time - 604800000;
let day_start_time = current_time - 86400000; let day_start_time = current_time - 86400000;
let fee = 0; let fee = 0;
resBody.payments.sent.forEach(sentEle => { body.relayed.forEach(relayedEle => {
logger.info({fileName: 'Fees', msg: 'Fee Relayed Transaction: ' + JSON.stringify(relayedEle)});
fee = Math.round((relayedEle.amountIn - relayedEle.amountOut)/1000);
if (relayedEle.timestamp >= day_start_time) {
fees.daily_fee = fees.daily_fee + fee;
fees.daily_txs = fees.daily_txs + 1;
}
if (relayedEle.timestamp >= week_start_time) {
fees.weekly_fee = fees.weekly_fee + fee;
fees.weekly_txs = fees.weekly_txs + 1;
}
if (relayedEle.timestamp >= month_start_time) {
fees.monthly_fee = fees.monthly_fee + fee;
fees.monthly_txs = fees.monthly_txs + 1;
}
});
logger.info({fileName: 'Fees', msg: JSON.stringify(fees)});
res.status(200).json(fees);
})
.catch(errRes => {
let err = JSON.parse(JSON.stringify(errRes));
if (err.options && err.options.headers && err.options.headers.authorization) {
delete err.options.headers.authorization;
}
if (err.response && err.response.request && err.response.request.headers && err.response.request.headers.authorization) {
delete err.response.request.headers.authorization;
}
logger.error({fileName: 'Fees', lineNum: 51, msg: 'Get Fees Error: ' + JSON.stringify(err)});
return res.status(err.statusCode ? err.statusCode : 500).json({
message: "Fetching Fees failed!",
error: err.error && err.error.error ? err.error.error : err.error ? err.error : "Unknown Server Error"
});
});
};
exports.getPayments = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/audit';
options.form = null;
request.post(options).then((body) => {
logger.info({fileName: 'Fees', msg: 'Payments Response: ' + JSON.stringify(body)});
let payments = {
sent: body && body.sent ? body.sent : [],
received: body && body.received ? body.received : [],
relayed: body && body.relayed ? body.relayed : []
};
payments.sent.forEach(sentEle => {
if (sentEle.recipientAmount) { sentEle.recipientAmount = Math.round(sentEle.recipientAmount/1000); } if (sentEle.recipientAmount) { sentEle.recipientAmount = Math.round(sentEle.recipientAmount/1000); }
if (sentEle.parts && sentEle.parts.length > 0) { if (sentEle.parts && sentEle.parts.length > 0) {
sentEle.firstPartTimestamp = sentEle.parts[0].timestamp; sentEle.firstPartTimestamp = sentEle.parts[0].timestamp;
@ -39,7 +78,7 @@ exports.getFees = (req, res, next) => {
if (part.feesPaid) { part.feesPaid = Math.round(part.feesPaid/1000); } if (part.feesPaid) { part.feesPaid = Math.round(part.feesPaid/1000); }
}); });
}); });
resBody.payments.received.forEach(receivedEle => { payments.received.forEach(receivedEle => {
if (receivedEle.parts && receivedEle.parts.length > 0) { if (receivedEle.parts && receivedEle.parts.length > 0) {
receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp; receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp;
receivedEle.firstPartTimestampStr = (!receivedEle.firstPartTimestamp) ? '' : common.convertTimestampToDate(Math.round(receivedEle.firstPartTimestamp / 1000)); receivedEle.firstPartTimestampStr = (!receivedEle.firstPartTimestamp) ? '' : common.convertTimestampToDate(Math.round(receivedEle.firstPartTimestamp / 1000));
@ -49,30 +88,17 @@ exports.getFees = (req, res, next) => {
if (part.amount) { part.amount = Math.round(part.amount/1000); } if (part.amount) { part.amount = Math.round(part.amount/1000); }
}); });
}); });
resBody.payments.relayed.forEach(relayedEle => { payments.relayed.forEach(relayedEle => {
logger.info({fileName: 'Fees', msg: 'Relayed Transaction: ' + JSON.stringify(relayedEle)}); logger.info({fileName: 'Fees', msg: 'Payment Relayed Transaction: ' + JSON.stringify(relayedEle)});
relayedEle.timestampStr = (!relayedEle.timestamp) ? '' : common.convertTimestampToDate(Math.round(relayedEle.timestamp / 1000)); relayedEle.timestampStr = (!relayedEle.timestamp) ? '' : common.convertTimestampToDate(Math.round(relayedEle.timestamp / 1000));
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); }
fee = relayedEle.amountIn - relayedEle.amountOut;
if (relayedEle.timestamp >= day_start_time) {
resBody.fees.daily_fee = resBody.fees.daily_fee + fee;
resBody.fees.daily_txs = resBody.fees.daily_txs + 1;
}
if (relayedEle.timestamp >= week_start_time) {
resBody.fees.weekly_fee = resBody.fees.weekly_fee + fee;
resBody.fees.weekly_txs = resBody.fees.weekly_txs + 1;
}
if (relayedEle.timestamp >= month_start_time) {
resBody.fees.monthly_fee = resBody.fees.monthly_fee + fee;
resBody.fees.monthly_txs = resBody.fees.monthly_txs + 1;
}
}); });
resBody.payments.sent = common.sortDescByKey(resBody.payments.sent, 'firstPartTimestamp'); payments.sent = common.sortDescByKey(payments.sent, 'firstPartTimestamp');
resBody.payments.received = common.sortDescByKey(resBody.payments.received, 'firstPartTimestamp'); payments.received = common.sortDescByKey(payments.received, 'firstPartTimestamp');
resBody.payments.relayed = common.sortDescByKey(resBody.payments.relayed, 'timestamp'); payments.relayed = common.sortDescByKey(payments.relayed, 'timestamp');
logger.info({fileName: 'Fees', msg: JSON.stringify(resBody)}); logger.info({fileName: 'Fees', msg: JSON.stringify(payments)});
res.status(200).json(resBody); res.status(200).json(payments);
}) })
.catch(errRes => { .catch(errRes => {
let err = JSON.parse(JSON.stringify(errRes)); let err = JSON.parse(JSON.stringify(errRes));
@ -82,9 +108,9 @@ exports.getFees = (req, res, next) => {
if (err.response && err.response.request && err.response.request.headers && err.response.request.headers.authorization) { if (err.response && err.response.request && err.response.request.headers && err.response.request.headers.authorization) {
delete err.response.request.headers.authorization; delete err.response.request.headers.authorization;
} }
logger.error({fileName: 'Fees', lineNum: 57, msg: 'Get Fees Error: ' + JSON.stringify(err)}); logger.error({fileName: 'Fees', lineNum: 113, msg: 'Get Payments Error: ' + JSON.stringify(err)});
return res.status(err.statusCode ? err.statusCode : 500).json({ return res.status(err.statusCode ? err.statusCode : 500).json({
message: "Fetching Fees failed!", message: "Fetching Payments failed!",
error: err.error && err.error.error ? err.error.error : err.error ? err.error : "Unknown Server Error" error: err.error && err.error.error ? err.error.error : err.error ? err.error : "Unknown Server Error"
}); });
}); });

@ -5,6 +5,7 @@ var options = {};
var pendingInvoices = []; var pendingInvoices = [];
getReceivedPaymentInfo = (invoice) => { getReceivedPaymentInfo = (invoice) => {
logger.info({fileName: 'Invoice', msg: 'Invoice Received: ' + JSON.stringify(invoice)});
let idx = -1; let idx = -1;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
invoice.timestampStr = (!invoice.timestamp) ? '' : common.convertTimestampToDate(invoice.timestamp); invoice.timestampStr = (!invoice.timestamp) ? '' : common.convertTimestampToDate(invoice.timestamp);
@ -17,10 +18,10 @@ getReceivedPaymentInfo = (invoice) => {
options.form = { paymentHash: invoice.paymentHash }; options.form = { paymentHash: invoice.paymentHash };
request(options).then(response => { request(options).then(response => {
invoice.status = response.status.type; invoice.status = response.status.type;
invoice.amount = Math.round(response.status.amount/1000); if (response.status && response.status.type === 'received') {
if (response.status.receivedAt) { invoice.amountSettled = response.status.amount ? Math.round(response.status.amount/1000) : 0;
invoice.receivedAt = Math.round(response.status.receivedAt / 1000); invoice.receivedAt = response.status.receivedAt ? Math.round(response.status.receivedAt / 1000) : 0;
invoice.receivedAtStr = common.convertTimestampToDate(invoice.receivedAt); invoice.receivedAtStr = response.status.receivedAt ? common.convertTimestampToDate(invoice.receivedAt) : '';
} }
resolve(invoice); resolve(invoice);
}).catch(err => { }).catch(err => {

@ -105,3 +105,78 @@ exports.queryPaymentRoute = (req, res, next) => {
}); });
}); });
}; };
exports.getSentPaymentInformation = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/getsentinfo';
options.form = { paymentHash: req.params.paymentHash };
request.post(options).then((body) => {
logger.info({fileName: 'Payments', msg: 'Payment Sent Information Received: ' + JSON.stringify(body)});
body.forEach(sentPayment => {
sentPayment.createdAtStr = (!sentPayment.createdAt) ? '' : common.convertTimestampToDate(sentPayment.createdAt);
if (sentPayment.amount) { sentPayment.amount = Math.round(sentPayment.amount/1000); }
if (sentPayment.recipientAmount) { sentPayment.recipientAmount = Math.round(sentPayment.recipientAmount/1000); }
});
res.status(200).json(body);
})
.catch(errRes => {
let err = JSON.parse(JSON.stringify(errRes));
if (err.options && err.options.headers && err.options.headers.authorization) {
delete err.options.headers.authorization;
}
if (err.response && err.response.request && err.response.request.headers && err.response.request.headers.authorization) {
delete err.response.request.headers.authorization;
}
logger.error({fileName: 'Payments', lineNum: 131, msg: 'Payment Sent Information Error: ' + JSON.stringify(err)});
return res.status(err.statusCode ? err.statusCode : 500).json({
message: "Payment Sent Information Failed!",
error: err.error && err.error.error ? err.error.error : err.error ? err.error : "Unknown Server Error"
});
});
};
exports.getSentPaymentsInformation = (req, res, next) => {
options = common.getOptions();
if (req.body.payments) {
let paymentsArr = req.body.payments.split(',');
Promise.all(paymentsArr.map(payment => {return getSentInfoFromPaymentRequest(payment)}))
.then(function(values) {
logger.info({fileName: 'Payments', msg: 'Payment Sent Informations: ' + JSON.stringify(values)});
res.status(200).json(values);
})
.catch(errRes => {
let err = JSON.parse(JSON.stringify(errRes));
if (err.options && err.options.headers && err.options.headers.authorization) {
delete err.options.headers.authorization;
}
if (err.response && err.response.request && err.response.request.headers && err.response.request.headers.authorization) {
delete err.response.request.headers.authorization;
}
logger.error({fileName: 'Payments', lineNum: 158, msg: 'Payment Sent Information Error: ' + JSON.stringify(err)});
return res.status(err.statusCode ? err.statusCode : 500).json({
message: "Payment Sent Information Failed!",
error: err.error && err.error.error ? err.error.error : err.error ? err.error : "Unknown Server Error"
});
});
} else {
res.status(200).json([]);
}
};
getSentInfoFromPaymentRequest = (payment) => {
options.url = common.getSelLNServerUrl() + '/getsentinfo';
options.form = { paymentHash: payment };
return new Promise(function(resolve, reject) {
request.post(options).then((body) => {
logger.info({fileName: 'Payments', msg: 'Payment Sent Information Received: ' + JSON.stringify(body)});
body.forEach(sentPayment => {
sentPayment.createdAtStr = (!sentPayment.createdAt) ? '' : common.convertTimestampToDate(sentPayment.createdAt);
if (sentPayment.amount) { sentPayment.amount = Math.round(sentPayment.amount/1000); }
if (sentPayment.recipientAmount) { sentPayment.recipientAmount = Math.round(sentPayment.recipientAmount/1000); }
});
resolve(body);
})
.catch(err => resolve(err));
});
}

@ -69,7 +69,12 @@ exports.connectPeer = (req, res, next) => {
if (body === 'already connected') { if (body === 'already connected') {
return res.status(500).json({ return res.status(500).json({
message: "Connect Peer Failed!", message: "Connect Peer Failed!",
error: "already connected" error: "Already connected"
});
} else if (typeof body === 'string' && body.includes('connection failed')) {
return res.status(500).json({
message: "Connect Peer Failed!",
error: body.charAt(0).toUpperCase() + body.slice(1)
}); });
} }
options.url = common.getSelLNServerUrl() + '/peers'; options.url = common.getSelLNServerUrl() + '/peers';
@ -87,7 +92,7 @@ exports.connectPeer = (req, res, next) => {
peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20); peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20);
}); });
let peers = (body) ? common.sortDescByStrKey(body, 'alias') : []; let peers = (body) ? common.sortDescByStrKey(body, 'alias') : [];
peers = common.newestOnTop(peers, 'nodeId', req.query.uri ? req.query.uri.substring(0, req.query.uri.indexOf('@')) : req.query.nodeId ? req.query.nodeId : ''); peers = common.newestOnTop(peers, 'nodeId', req.query.nodeId ? req.query.nodeId : '');
logger.info({fileName: 'Peers', msg: 'Peer with Newest On Top: ' + JSON.stringify(peers)}); logger.info({fileName: 'Peers', msg: 'Peer with Newest On Top: ' + JSON.stringify(peers)});
logger.info({fileName: 'Peers', msg: 'Peer Added Successfully'}); logger.info({fileName: 'Peers', msg: 'Peer Added Successfully'});
res.status(201).json(peers); res.status(201).json(peers);

@ -262,8 +262,9 @@ exports.getRemoteFeePolicy = (req, res, next) => {
exports.getAliasesForPubkeys = (req, res, next) => { exports.getAliasesForPubkeys = (req, res, next) => {
options = common.getOptions(); options = common.getOptions();
if (req.params.pubKeys.length && req.params.pubKeys.length > 0) { if (req.query.pubkeys) {
Promise.all(req.params.pubKeys.map(pubkey => {return getAliasFromPubkey(pubkey)})) let pubkeyArr = req.query.pubkeys.split(',');
Promise.all(pubkeyArr.map(pubkey => {return getAliasFromPubkey(pubkey)}))
.then(function(values) { .then(function(values) {
logger.info({fileName: 'Graph', msg: 'Node Alias: ' + JSON.stringify(values)}); logger.info({fileName: 'Graph', msg: 'Node Alias: ' + JSON.stringify(values)});
res.status(200).json(values); res.status(200).json(values);

@ -2,13 +2,12 @@ var request = require('request-promise');
var common = require('../../common'); var common = require('../../common');
var logger = require('../logger'); var logger = require('../logger');
var options = {}; var options = {};
var swapServerUrl = '';
exports.loopOut = (req, res, next) => { exports.loopOut = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/out'; options.url = options.url + '/v1/loop/out';
let body = { options.body = {
amt: req.body.amount, amt: req.body.amount,
sweep_conf_target: req.body.targetConf, sweep_conf_target: req.body.targetConf,
max_swap_routing_fee: req.body.swapRoutingFee, max_swap_routing_fee: req.body.swapRoutingFee,
@ -18,20 +17,19 @@ exports.loopOut = (req, res, next) => {
max_swap_fee: req.body.swapFee, max_swap_fee: req.body.swapFee,
swap_publication_deadline: req.body.swapPublicationDeadline swap_publication_deadline: req.body.swapPublicationDeadline
}; };
if (req.body.chanId !== '') { body['loop_out_channel'] = req.body.chanId; } if (req.body.chanId !== '') { options.body['loop_out_channel'] = req.body.chanId; }
if (req.body.destAddress !== '') { body['dest'] = req.body.destAddress; } if (req.body.destAddress !== '') { options.body['dest'] = req.body.destAddress; }
options.body = JSON.stringify(body); logger.info({fileName: 'Loop', msg: 'Loop Out Body: ' + JSON.stringify(options.body)});
logger.info({fileName: 'Loop', msg: 'Loop Out Body: ' + options.body}); request.post(options).then(loopOutRes => {
request.post(options).then(function (body) { logger.info({fileName: 'Loop', msg: 'Loop Out: ' + JSON.stringify(loopOutRes)});
logger.info({fileName: 'Loop', msg: 'Loop Out: ' + JSON.stringify(body)}); if(!loopOutRes || loopOutRes.error) {
if(!body || body.error) { logger.error({fileName: 'Loop', lineNum: 28, msg: 'Loop Out Error: ' + JSON.stringify(loopOutRes.error)});
logger.error({fileName: 'Loop', lineNum: 28, msg: 'Loop Out Error: ' + JSON.stringify(body.error)});
res.status(500).json({ res.status(500).json({
message: 'Loop Out Failed!', message: 'Loop Out Failed!',
error: (!body) ? 'Error From Server!' : body.error.message error: (!loopOutRes) ? 'Error From Server!' : loopOutRes.error.message
}); });
} else { } else {
res.status(201).json(body); res.status(201).json(loopOutRes);
} }
}) })
.catch(errRes => { .catch(errRes => {
@ -51,9 +49,9 @@ exports.loopOut = (req, res, next) => {
}; };
exports.loopOutTerms = (req, res, next) => { exports.loopOutTerms = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Terms Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop Out Terms Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/out/terms'; options.url = options.url + '/v1/loop/out/terms';
request(options).then(function (body) { request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Out Terms: ' + JSON.stringify(body)}); logger.info({fileName: 'Loop', msg: 'Loop Out Terms: ' + JSON.stringify(body)});
res.status(200).json(body); res.status(200).json(body);
@ -75,16 +73,15 @@ exports.loopOutTerms = (req, res, next) => {
}; };
exports.loopOutQuote = (req, res, next) => { exports.loopOutQuote = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Quote Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop Out Quote Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/out/quote/' + req.params.amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; options.url = options.url + '/v1/loop/out/quote/' + req.params.amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
logger.info({fileName: 'Loop', msg: 'Loop Out Quote URL: ' + options.url}); logger.info({fileName: 'Loop', msg: 'Loop Out Quote URL: ' + options.url});
request(options).then(function (body) { request(options).then(function (quoteRes) {
logger.info({fileName: 'Loop', msg: 'Loop Out Quote: ' + body}); logger.info({fileName: 'Loop', msg: 'Loop Out Quote: ' + JSON.stringify(quoteRes)});
body = JSON.parse(body); quoteRes.amount = +req.params.amount;
body.amount = +req.params.amount; quoteRes.swap_payment_dest = quoteRes.swap_payment_dest ? Buffer.from(quoteRes.swap_payment_dest, 'base64').toString('hex') : '';
body.swap_payment_dest = body.swap_payment_dest ? Buffer.from(body.swap_payment_dest, 'base64').toString('hex') : ''; res.status(200).json(quoteRes);
res.status(200).json(body);
}) })
.catch(errRes => { .catch(errRes => {
let err = JSON.parse(JSON.stringify(errRes)); let err = JSON.parse(JSON.stringify(errRes));
@ -103,20 +100,17 @@ exports.loopOutQuote = (req, res, next) => {
}; };
exports.loopOutTermsAndQuotes = (req, res, next) => { exports.loopOutTermsAndQuotes = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Terms And Quotes Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop Out Terms And Quotes Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/out/terms'; options.url = options.url + '/v1/loop/out/terms';
request(options).then(function(terms) { request(options).then(function(terms) {
logger.info({fileName: 'Loop', msg: 'Loop Out Terms: ' + JSON.stringify(terms)}); logger.info({fileName: 'Loop', msg: 'Loop Out Terms: ' + JSON.stringify(terms)});
const options1 = {}; const options2 = {}; const options1 = common.getSwapServerOptions(); const options2 = common.getSwapServerOptions();
terms = JSON.parse(terms); options1.url = options1.url + '/v1/loop/out/quote/' + terms.min_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
options1.url = swapServerUrl + '/v1/loop/out/quote/' + terms.min_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; options2.url = options2.url + '/v1/loop/out/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
options2.url = swapServerUrl + '/v1/loop/out/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
logger.info({fileName: 'Loop', msg: 'Loop Out Min Quote Options: ' + JSON.stringify(options1)}); logger.info({fileName: 'Loop', msg: 'Loop Out Min Quote Options: ' + JSON.stringify(options1)});
logger.info({fileName: 'Loop', msg: 'Loop Out Max Quote Options: ' + JSON.stringify(options2)}); logger.info({fileName: 'Loop', msg: 'Loop Out Max Quote Options: ' + JSON.stringify(options2)});
Promise.all([request(options1), request(options2)]).then(function(values) { Promise.all([request(options1), request(options2)]).then(function(values) {
values[0] = JSON.parse(values[0]);
values[1] = JSON.parse(values[1]);
values[0].amount = +terms.min_swap_amount; values[0].amount = +terms.min_swap_amount;
values[1].amount = +terms.max_swap_amount; values[1].amount = +terms.max_swap_amount;
values[0].swap_payment_dest = values[0].swap_payment_dest ? Buffer.from(values[0].swap_payment_dest, 'base64').toString('hex') : ''; values[0].swap_payment_dest = values[0].swap_payment_dest ? Buffer.from(values[0].swap_payment_dest, 'base64').toString('hex') : '';
@ -157,14 +151,14 @@ exports.loopOutTermsAndQuotes = (req, res, next) => {
}; };
exports.loopIn = (req, res, next) => { exports.loopIn = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop In Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop In Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/in'; options.url = options.url + '/v1/loop/in';
options.body = JSON.stringify({ options.body = {
amt: req.body.amount, amt: req.body.amount,
max_swap_fee: req.body.swapFee, max_swap_fee: req.body.swapFee,
max_miner_fee: req.body.minerFee max_miner_fee: req.body.minerFee
}); };
request.post(options).then(function (body) { request.post(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In: ' + JSON.stringify(body)}); logger.info({fileName: 'Loop', msg: 'Loop In: ' + JSON.stringify(body)});
if(!body || body.error) { if(!body || body.error) {
@ -194,9 +188,9 @@ exports.loopIn = (req, res, next) => {
}; };
exports.loopInTerms = (req, res, next) => { exports.loopInTerms = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop In Terms Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop In Terms Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/in/terms'; options.url = options.url + '/v1/loop/in/terms';
request(options).then(function (body) { request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In Terms: ' + JSON.stringify(body)}); logger.info({fileName: 'Loop', msg: 'Loop In Terms: ' + JSON.stringify(body)});
res.status(200).json(body); res.status(200).json(body);
@ -218,13 +212,12 @@ exports.loopInTerms = (req, res, next) => {
}; };
exports.loopInQuote = (req, res, next) => { exports.loopInQuote = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop In Quote Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop In Quote Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/in/quote/' + req.params.amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; options.url = options.url + '/v1/loop/in/quote/' + req.params.amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
logger.info({fileName: 'Loop', msg: 'Loop In Quote Options: ' + options.url}); logger.info({fileName: 'Loop', msg: 'Loop In Quote Options: ' + options.url});
request(options).then(function (body) { request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In Quote: ' + JSON.stringify(body)}); logger.info({fileName: 'Loop', msg: 'Loop In Quote: ' + JSON.stringify(body)});
body = JSON.parse(body);
body.amount = +req.params.amount; body.amount = +req.params.amount;
body.swap_payment_dest = body.swap_payment_dest ? Buffer.from(body.swap_payment_dest, 'base64').toString('hex') : ''; body.swap_payment_dest = body.swap_payment_dest ? Buffer.from(body.swap_payment_dest, 'base64').toString('hex') : '';
res.status(200).json(body); res.status(200).json(body);
@ -246,20 +239,17 @@ exports.loopInQuote = (req, res, next) => {
}; };
exports.loopInTermsAndQuotes = (req, res, next) => { exports.loopInTermsAndQuotes = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop In Terms And Quotes Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop In Terms And Quotes Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/in/terms'; options.url = options.url + '/v1/loop/in/terms';
request(options).then(function(terms) { request(options).then(function(terms) {
logger.info({fileName: 'Loop', msg: 'Loop In Terms: ' + JSON.stringify(terms)}); logger.info({fileName: 'Loop', msg: 'Loop In Terms: ' + JSON.stringify(terms)});
const options1 = {}; const options2 = {}; const options1 = common.getSwapServerOptions(); const options2 = common.getSwapServerOptions();
terms = JSON.parse(terms); options1.url = options1.url + '/v1/loop/in/quote/' + terms.min_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
options1.url = swapServerUrl + '/v1/loop/in/quote/' + terms.min_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline; options2.url = options2.url + '/v1/loop/in/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
options2.url = swapServerUrl + '/v1/loop/in/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
logger.info({fileName: 'Loop', msg: 'Loop In Min Quote Options: ' + JSON.stringify(options1)}); logger.info({fileName: 'Loop', msg: 'Loop In Min Quote Options: ' + JSON.stringify(options1)});
logger.info({fileName: 'Loop', msg: 'Loop In Max Quote Options: ' + JSON.stringify(options2)}); logger.info({fileName: 'Loop', msg: 'Loop In Max Quote Options: ' + JSON.stringify(options2)});
Promise.all([request(options1), request(options2)]).then(function(values) { Promise.all([request(options1), request(options2)]).then(function(values) {
values[0] = JSON.parse(values[0]);
values[1] = JSON.parse(values[1]);
values[0].amount = +terms.min_swap_amount; values[0].amount = +terms.min_swap_amount;
values[1].amount = +terms.max_swap_amount; values[1].amount = +terms.max_swap_amount;
values[0].swap_payment_dest = values[0].swap_payment_dest ? Buffer.from(values[0].swap_payment_dest, 'base64').toString('hex') : ''; values[0].swap_payment_dest = values[0].swap_payment_dest ? Buffer.from(values[0].swap_payment_dest, 'base64').toString('hex') : '';
@ -300,12 +290,11 @@ exports.loopInTermsAndQuotes = (req, res, next) => {
}; };
exports.swaps = (req, res, next) => { exports.swaps = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/swaps'; options.url = options.url + '/v1/loop/swaps';
request(options).then(function (body) { request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Swaps: ' + body}); logger.info({fileName: 'Loop', msg: 'Loop Swaps: ' + JSON.stringify(body)});
body = JSON.parse(body);
if (body.swaps && body.swaps.length > 0) { if (body.swaps && body.swaps.length > 0) {
body.swaps.forEach(swap => { body.swaps.forEach(swap => {
swap.initiation_time_str = (!swap.initiation_time) ? '' : common.convertTimestampToDate(Math.round(swap.initiation_time/1000000000)); swap.initiation_time_str = (!swap.initiation_time) ? '' : common.convertTimestampToDate(Math.round(swap.initiation_time/1000000000));
@ -324,7 +313,7 @@ exports.swaps = (req, res, next) => {
if (err.response && err.response.request && err.response.request.headers && err.response.request.headers['Grpc-Metadata-macaroon']) { if (err.response && err.response.request && err.response.request.headers && err.response.request.headers['Grpc-Metadata-macaroon']) {
delete err.response.request.headers['Grpc-Metadata-macaroon']; delete err.response.request.headers['Grpc-Metadata-macaroon'];
} }
logger.error({fileName: 'Loop', lineNum: 316, msg: 'List Swaps Error: ' + JSON.stringify(err)}); logger.error({fileName: 'Loop', lineNum: 327, msg: 'List Swaps Error: ' + JSON.stringify(err)});
return res.status(500).json({ return res.status(500).json({
message: "Loop Swaps Failed!", message: "Loop Swaps Failed!",
error: (err.error && err.error.error) ? err.error.error : (err.error) ? err.error : err error: (err.error && err.error.error) ? err.error.error : (err.error) ? err.error : err
@ -333,12 +322,11 @@ exports.swaps = (req, res, next) => {
}; };
exports.swap = (req, res, next) => { exports.swap = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl(); options = common.getSwapServerOptions();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); } if(options.url === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/v1/loop/swap/' + req.params.id; options.url = options.url + '/v1/loop/swap/' + req.params.id;
request(options).then(function (body) { request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Swap: ' + body}); logger.info({fileName: 'Loop', msg: 'Loop Swap: ' + body});
body = JSON.parse(body);
body.initiation_time_str = (!body.initiation_time) ? '' : common.convertTimestampToDate(Math.round(body.initiation_time/1000000000)); body.initiation_time_str = (!body.initiation_time) ? '' : common.convertTimestampToDate(Math.round(body.initiation_time/1000000000));
body.last_update_time_str = (!body.last_update_time) ? '' : common.convertTimestampToDate(Math.round(body.last_update_time/1000000000)); body.last_update_time_str = (!body.last_update_time) ? '' : common.convertTimestampToDate(Math.round(body.last_update_time/1000000000));
res.status(200).json(body); res.status(200).json(body);

@ -37,3 +37,43 @@ exports.decodePayment = (req, res, next) => {
}); });
}); });
}; };
exports.decodePayments = (req, res, next) => {
options = common.getOptions();
if (req.body.payments) {
let paymentsArr = req.body.payments.split(',');
Promise.all(paymentsArr.map(payment => {return decodePaymentFromPaymentRequest(payment)}))
.then(function(values) {
logger.info({fileName: 'PayReq', msg: 'Decoded Payments: ' + JSON.stringify(values)});
res.status(200).json(values);
})
.catch(errRes => {
let err = JSON.parse(JSON.stringify(errRes));
if (err.options && err.options.headers && err.options.headers['Grpc-Metadata-macaroon']) {
delete err.options.headers['Grpc-Metadata-macaroon'];
}
if (err.response && err.response.request && err.response.request.headers && err.response.request.headers['Grpc-Metadata-macaroon']) {
delete err.response.request.headers['Grpc-Metadata-macaroon'];
}
logger.error({fileName: 'PayReq', lineNum: 58, msg: 'Decode Payments Failed: ' + JSON.stringify(err)});
return res.status(500).json({
message: "Decode Payments Failed!",
error: err.error
});
});
} else {
res.status(200).json([]);
}
};
decodePaymentFromPaymentRequest = (payment) => {
return new Promise(function(resolve, reject) {
options.url = common.getSelLNServerUrl() + '/v1/payreq/' + payment;
request(options)
.then(function(res) {
logger.info({fileName: 'PayReq', msg: 'Description: ' + JSON.stringify(res.description)});
resolve(res);
})
.catch(err => resolve({}));
});
}

@ -117,3 +117,25 @@ exports.updateSelNodeOptions = (req, res, next) => {
let response = common.updateSelectedNodeOptions(); let response = common.updateSelectedNodeOptions();
res.status(response.status).json({updateMessage: response.message}); res.status(response.status).json({updateMessage: response.message});
} }
exports.getUTXOs = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/v2/wallet/utxos?max_confs=' + req.query.max_confs;
request.post(options).then((body) => {
res.status(200).json(body.utxos ? body.utxos : []);
})
.catch(errRes => {
let err = JSON.parse(JSON.stringify(errRes));
if (err.options && err.options.headers && err.options.headers['Grpc-Metadata-macaroon']) {
delete err.options.headers['Grpc-Metadata-macaroon'];
}
if (err.response && err.response.request && err.response.request.headers && err.response.request.headers['Grpc-Metadata-macaroon']) {
delete err.response.request.headers['Grpc-Metadata-macaroon'];
}
logger.error({fileName: 'Wallet', lineNum: 143, msg: 'UTXOs Error: ' + JSON.stringify(err)});
return res.status(500).json({
message: "UTXO list failed!",
error: err.error
});
});
}

@ -26,7 +26,7 @@ parameters have `default` values for initial setup and can be updated after RTL
"Settings": { "Settings": {
"userPersona": "<User persona to tailor the data on UI. Allowed values MERCHANT, OPERATOR. Default MERCHANT, Required>", "userPersona": "<User persona to tailor the data on UI. Allowed values MERCHANT, OPERATOR. Default MERCHANT, Required>",
"themeMode": "<Theme modes, Allowed values DAY, NIGHT. Default DAY, Required>", "themeMode": "<Theme modes, Allowed values DAY, NIGHT. Default DAY, Required>",
"themeColor": "<Theme colors, Allowed values PURPLE, TEAL, INDIGO, PINK. Default PURPLE, Required>", "themeColor": "<Theme colors, Allowed values PURPLE, TEAL, INDIGO, PINK, YELLOW. Default PURPLE, Required>",
"channelBackupPath": "<Path to save channel backup file. Only for LND implementation, Default <RTL root>\backup\node-1, Optional>", "channelBackupPath": "<Path to save channel backup file. Only for LND implementation, Default <RTL root>\backup\node-1, Optional>",
"bitcoindConfigPath": "<Path of bitcoind.conf path if available locally>", "bitcoindConfigPath": "<Path of bitcoind.conf path if available locally>",
"enableLogging": <Parameter to turn RTL logging off/on. Allowed values - true, false, default false, Required>, "enableLogging": <Parameter to turn RTL logging off/on. Allowed values - true, false, default false, Required>,

@ -0,0 +1,59 @@
## Contributing to RTL
Thanks for your interest in contributing to the development of RTL. RTL is a community project and aspires to remain free and open source for the benefit of the community. With that objective in mind, this document provides contribution guidelines which the community can utilize to contribute towards the development and maintenance of this software.
Table Of Contents
* [How Can I Contribute](#how)
* [Getting Started](#start)
* [Dependencies](#dependency)
* [UI & UX Design Decisions](#design)
* [Style Guidlines](#guidlines)
### <a name="how"></a>How Can I Contribute
There are multiple ways you can contribute towards the development and not all of those methods involve coding. Below are a few examples on how meaningful contributions can be made.
* [Bug Report](#bug) - While using RTL, if you notice something is not working correctly create a bug report, by creating an issue.
* [Feature Request](#feature) - While using RTL, if you feel that the software should be changed in certain way to make it for usable and helpful, create a feature request.
* [Testing](#testing) - Testing is one the easiest and most sought after method of contribution. Testing can be done on release branches, so that releases are relatively bug free.
* [Design](#design) - Design inputs can be made based on user enhancement suggestions or novel ideas which you get while using RTL.
* [Code](#code) - Development contributions are made via making coding changes to the software and getting it tested, reviewed and merged.
#### <a name="bug"></a>Bug Report
Bug reports are reports of technical or functional issues with the software. Bug reports help with the removal of defects from the software and improve its quality. Guidelines for submitting a bug report:
* Label the bug with the correct Lightning implementation (LND/C-Lightning/Eclair).
* Add the `Bug` label to the issue
* Provide details of your configuration like Device, Operating system, Bitcoin version, Lightning implementation version, RTL version etc.
* Attempt to explain the scenario in detail, so that the developer can try to replicate the issue at their end.
* If the bug is with the UI, screenshots help. Try to highlight the problem areas by circling with red outline.
* Take care to redact sensitive info from the screenshots like Pubkey or channel IDs etc.
* Be responsive to the developers requesting details on the issues.
#### <a name="feature"></a>Feature Request
Feature Requests are requests raised to add new features to the application. The features requests can range from technical to functional, making the application better for everyone. Guidelines to follow for create a feature request:
* Label the feature request with the correct Lightning implementation (LND/C-Lightning/Eclair).
* Add the `Enhancement Request` label to the issue
* If the feature relates to an existing aspect of the application, indicate clearly which part of the application the feature request relates to. E.g. Transactions page under Lightning menu.
* Provide the justification for the feature request. E.g. Privacy/Security/Usability benefit.
* If the feature request is technical in nature, try to provide the platform detail like OS, Lightning Implementation version etc.
* For new UI features mockups are helpful for the developers.
* Be responsive on the feature requests when developers request details or clarification and also help with the testing of the features requested.
#### <a name="testing"></a>Testing
Testing is the easiest and most effective method to contribute. It helps uncover bugs and improve the quality of software. Best time to test would be pre-release, when the changes are being made to the software for the next release. RTL maintains a release branch for the next planned release and changes are merge to the release branch on a regular basis. The testers can contribute by pulling from the release branch and testing the software. If issues are found during testing, follow the steps described above to raise bug reports to help address the issues.
#### <a name="design"></a>Design
Design suggestions are always welcome and helpful. Design suggestion can range from improving both the aesthetics as well as the UX of the application. We believe improving design and UX of the application is an ongoing journey. User feedback and bugs raised also provide insights into how both can be improved. if you would like to provide design related suggestions or contribute with design inputs, raise issues on the [Design repo of RTL](https://github.com/Ride-The-Lightning/RTL-Design) and follow the guidance provided there.
#### <a name="code"></a>Code
Contributions via code is the most sought after contribution and something we enthusiastically encourage. Follow the below guideline to be able to contribute code to RTL.
##### Pull Code
##### Install Dependencies
##### Node Server
##### Angular Server for Development
##### Package Angular Updates
##### Create a New Branch
##### Caution about adding new libraries
* We are very conservative in adding new dependencies. Do your best to not add any new libraries on RTL. This is the best strategy to keep the software safe from adding new vulnerabilites.
* Confirm before starting by creating an issue about the adding the library
* The library should be popular, well maintained and pre-existing vulnerability free.
##### Commit Updates
##### Create Pull Request

19
package-lock.json generated

@ -5705,6 +5705,11 @@
"minimalistic-crypto-utils": "^1.0.1" "minimalistic-crypto-utils": "^1.0.1"
} }
}, },
"hocon-parser": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hocon-parser/-/hocon-parser-1.0.1.tgz",
"integrity": "sha1-t5tmFDmZslXgi2c8gykbXjY/C3g="
},
"hosted-git-info": { "hosted-git-info": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz",
@ -7858,9 +7863,9 @@
} }
}, },
"node-forge": { "node-forge": {
"version": "0.9.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"dev": true "dev": true
}, },
"node-libs-browser": { "node-libs-browser": {
@ -10761,12 +10766,12 @@
} }
}, },
"selfsigned": { "selfsigned": {
"version": "1.10.7", "version": "1.10.8",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
"integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
"dev": true, "dev": true,
"requires": { "requires": {
"node-forge": "0.9.0" "node-forge": "^0.10.0"
} }
}, },
"semver": { "semver": {

@ -1,6 +1,6 @@
{ {
"name": "rtl", "name": "rtl",
"version": "0.9.0-beta", "version": "0.9.1-beta",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
@ -40,7 +40,8 @@
"cookie-parser": "^1.4.4", "cookie-parser": "^1.4.4",
"express": "^4.17.1", "express": "^4.17.1",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"ini": "^1.3.5", "hocon-parser": "1.0.1",
"ini": "1.3.5",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"ngx-perfect-scrollbar": "^9.0.0", "ngx-perfect-scrollbar": "^9.0.0",

@ -3,6 +3,7 @@ const express = require("express");
const router = express.Router(); const router = express.Router();
const authCheck = require("../authCheck"); const authCheck = require("../authCheck");
router.get("/", authCheck, FeesController.getFees); router.get("/fees", authCheck, FeesController.getFees);
router.get("/payments", authCheck, FeesController.getPayments);
module.exports = router; module.exports = router;

@ -5,6 +5,8 @@ const authCheck = require("../authCheck");
router.get("/route/", authCheck, PaymentsController.queryPaymentRoute); router.get("/route/", authCheck, PaymentsController.queryPaymentRoute);
router.get("/:invoice", authCheck, PaymentsController.decodePayment); router.get("/:invoice", authCheck, PaymentsController.decodePayment);
router.get("/getsentinfo/:paymentHash", authCheck, PaymentsController.getSentPaymentInformation);
router.post("/getsentinfos", authCheck, PaymentsController.getSentPaymentsInformation);
router.post("/", authCheck, PaymentsController.postPayment); router.post("/", authCheck, PaymentsController.postPayment);
module.exports = router; module.exports = router;

@ -5,7 +5,7 @@ const authCheck = require("../authCheck");
router.get("/", authCheck, graphController.getDescribeGraph); router.get("/", authCheck, graphController.getDescribeGraph);
router.get("/info", authCheck, graphController.getGraphInfo); router.get("/info", authCheck, graphController.getGraphInfo);
router.get("/nodes/:pubKeys", authCheck, graphController.getAliasesForPubkeys); router.get("/nodes", authCheck, graphController.getAliasesForPubkeys);
router.get("/node/:pubKey", authCheck, graphController.getGraphNode); router.get("/node/:pubKey", authCheck, graphController.getGraphNode);
router.get("/edge/:chanid", authCheck, graphController.getGraphEdge); router.get("/edge/:chanid", authCheck, graphController.getGraphEdge);
router.get("/edge/:chanid/:localPubkey", authCheck, graphController.getRemoteFeePolicy); router.get("/edge/:chanid/:localPubkey", authCheck, graphController.getRemoteFeePolicy);

@ -4,5 +4,6 @@ const router = express.Router();
const authCheck = require("../authCheck"); const authCheck = require("../authCheck");
router.get("/:payRequest", authCheck, PayRequestController.decodePayment); router.get("/:payRequest", authCheck, PayRequestController.decodePayment);
router.post("/", authCheck, PayRequestController.decodePayments);
module.exports = router; module.exports = router;

@ -5,6 +5,7 @@ const authCheck = require("../authCheck");
router.get("/genseed/:passphrase?", authCheck, WalletController.genSeed); router.get("/genseed/:passphrase?", authCheck, WalletController.genSeed);
router.get("/updateSelNodeOptions", authCheck, WalletController.updateSelNodeOptions); router.get("/updateSelNodeOptions", authCheck, WalletController.updateSelNodeOptions);
router.get("/getUTXOs", authCheck, WalletController.getUTXOs);
router.post("/:operation", authCheck, WalletController.operateWallet); router.post("/:operation", authCheck, WalletController.operateWallet);
module.exports = router; module.exports = router;

@ -1,10 +1,10 @@
<div fxLayout="column" id="rtl-container" class="rtl-container medium" [ngClass]="[settings.themeColor | lowercase, settings.themeMode | lowercase]"> <div fxLayout="column" id="rtl-container" class="rtl-container medium" [ngClass]="[settings.themeColor | lowercase, settings.themeMode | lowercase]">
<mat-toolbar fxLayout="row" fxLayoutAlign="space-between center" class="padding-gap-x bg-primary rtl-top-toolbar"> <mat-toolbar fxLayout="row" fxLayoutAlign="space-between center" class="padding-gap-x bg-primary rtl-top-toolbar">
<div> <div>
<button class="top-toolbar-icon mr-1" mat-icon-button (click)="sideNavToggle()" [matTooltip]="flgSideNavOpened ? 'Hide Navigation Menu' : 'Show Navigation Menu'" [matTooltipDisabled]="smallScreen"> <button class="top-toolbar-icon mr-1" mat-icon-button (click)="sideNavToggle()" [matTooltip]="flgSideNavOpened ? 'Hide Navigation Menu' : 'Show Navigation Menu'" [matTooltipDisabled]="smallScreen" matTooltipPosition="right">
<mat-icon>menu</mat-icon> <mat-icon>menu</mat-icon>
</button> </button>
<button *ngIf="!smallScreen" mat-icon-button (click)="flgSidenavPinned = !flgSidenavPinned" [matTooltip]="flgSidenavPinned ? 'Unpin Navigation Menu' : 'Pin Navigation Menu'"> <button *ngIf="!smallScreen" mat-icon-button (click)="flgSidenavPinned = !flgSidenavPinned" [matTooltip]="flgSidenavPinned ? 'Unpin Navigation Menu' : 'Pin Navigation Menu'" matTooltipPosition="right">
<svg class="top-toolbar-icon icon-pinned" viewBox="0 0 32 32"> <svg class="top-toolbar-icon icon-pinned" viewBox="0 0 32 32">
<path fill="currentColor" *ngIf="!flgSidenavPinned" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" /> <path fill="currentColor" *ngIf="!flgSidenavPinned" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
<path fill="currentColor" *ngIf="flgSidenavPinned" d="M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z" /> <path fill="currentColor" *ngIf="flgSidenavPinned" d="M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z" />

@ -2,42 +2,41 @@
<div fxFlex="100" class="padding-gap-large"> <div fxFlex="100" class="padding-gap-large">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between center" class="modal-info-header"> <mat-card-header fxLayout="row" fxLayoutAlign="space-between center" class="modal-info-header">
<div fxFlex="95" fxLayoutAlign="start start"> <div fxFlex="95" fxLayoutAlign="start start">
<span class="page-title">Send Payment</span> <span class="page-title">{{sweepAll ? 'Sweep All Funds' : 'Send Funds'}}</span>
</div> </div>
<button tabindex="8" fxFlex="5" fxLayoutAlign="center" class="btn-close-x p-0" [mat-dialog-close]="false" default mat-button>X</button> <button tabindex="8" fxFlex="5" fxLayoutAlign="center" class="btn-close-x p-0" [mat-dialog-close]="false" default mat-button>X</button>
</mat-card-header> </mat-card-header>
<mat-card-content class="mt-5px"> <mat-card-content class="mt-5px">
<form fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap overflow-x-hidden" (submit)="onSendFunds()" (reset)="resetData()" #form="ngForm"> <form fxLayout="row wrap" fxLayoutAlign="space-between start" fxFlex="100" *ngIf="!sweepAll; else sweepAllBlock;" class="padding-gap overflow-x-hidden" (submit)="onSendFunds()" (reset)="resetData()" #form="ngForm">
<div fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between start"> <mat-form-field fxFlex="55">
<mat-form-field fxFlex="55"> <input matInput autoFocus [(ngModel)]="transaction.address" placeholder="Bitcoin Address" tabindex="1" name="address" required #address="ngModel">
<input matInput autoFocus [(ngModel)]="transaction.address" placeholder="Bitcoin Address" tabindex="1" name="address" required #address="ngModel"> <mat-error *ngIf="!transaction.address">Bitcoin address is required.</mat-error>
<mat-error *ngIf="!transaction.address">Bitcoin address is required.</mat-error> </mat-form-field>
</mat-form-field> <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>
<span matSuffix> {{selAmountUnit}} </span> <span matSuffix> {{selAmountUnit}} </span>
<mat-error *ngIf="!transaction.satoshis">Amount is required.</mat-error> <mat-error *ngIf="!transaction.satoshis">Amount is required.</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex="10" fxLayoutAlign="start end"> <mat-form-field fxFlex="10" fxLayoutAlign="start end">
<mat-select [value]="selAmountUnit" tabindex="3" required name="amountUnit" (selectionChange)="onAmountUnitChange($event)" [disabled]="flgUseAllBalance"> <mat-select [value]="selAmountUnit" tabindex="3" required name="amountUnit" (selectionChange)="onAmountUnitChange($event)" [disabled]="flgUseAllBalance">
<mat-option *ngFor="let amountUnit of amountUnits" [value]="amountUnit">{{amountUnit}}</mat-option> <mat-option *ngFor="let amountUnit of amountUnits" [value]="amountUnit">{{amountUnit}}</mat-option>
</mat-select>
</mat-form-field>
<div fxFlex="60" fxLayoutAlign="space-between stretch" fxLayout="row wrap">
<mat-form-field fxFlex="48" fxLayoutAlign="start end">
<mat-select tabindex="6" placeholder="Fee Rate" [(value)]="transaction.feeRate" [disabled]="flgMinConf">
<mat-option *ngFor="let feeRateType of feeRateTypes" [value]="feeRateType.feeRateId">
{{feeRateType.feeRateType}}
</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div fxFlex="60" fxLayoutAlign="space-between stretch" fxLayout="row wrap"> <div fxFlex="48" fxLayout="row" fxLayoutAlign="start center">
<mat-form-field fxFlex="48" fxLayoutAlign="start end"> <mat-checkbox fxFlex="2" tabindex="7" color="primary" [(ngModel)]="flgMinConf" (change)="flgMinConf ? transaction.feeRate=null : transaction.minconf=null" name="flgMinConf" fxLayoutAlign="stretch start" class="mr-2"></mat-checkbox>
<mat-select tabindex="6" placeholder="Fee Rate" [(value)]="transaction.feeRate" [disabled]="flgMinConf"> <mat-form-field fxFlex="98">
<mat-option *ngFor="let feeRateType of feeRateTypes" [value]="feeRateType.feeRateId"> <input matInput [(ngModel)]="transaction.minconf" placeholder="Min Confirmation Blocks" type="number" name="blocks" step="1" min="0" tabindex="8" #blocks="ngModel" [required]="flgMinConf" [disabled]="!flgMinConf">
{{feeRateType.feeRateType}} <mat-error *ngIf="flgMinConf && !transaction.minconf">Min Confirmation Blocks is required.</mat-error>
</mat-option>
</mat-select>
</mat-form-field> </mat-form-field>
<div fxFlex="48" fxLayout="row" fxLayoutAlign="start center">
<mat-checkbox fxFlex="2" tabindex="7" color="primary" [(ngModel)]="flgMinConf" (change)="flgMinConf ? transaction.feeRate=null : transaction.minconf=null" name="flgMinConf" fxLayoutAlign="stretch start" class="mr-2"></mat-checkbox>
<mat-form-field fxFlex="98">
<input matInput [(ngModel)]="transaction.minconf" placeholder="Min Confirmation Blocks" type="number" name="blocks" step="1" min="0" tabindex="8" #blocks="ngModel" [required]="flgMinConf" [disabled]="!flgMinConf">
<mat-error *ngIf="flgMinConf && !transaction.minconf">Min Confirmation Blocks is required.</mat-error>
</mat-form-field>
</div>
</div> </div>
</div> </div>
<div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between stretch" *ngIf="isCompatibleVersion"> <div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between stretch" *ngIf="isCompatibleVersion">
@ -75,3 +74,76 @@
</mat-card-content> </mat-card-content>
</div> </div>
</div> </div>
<ng-template #sweepAllBlock>
<div fxLayout="column">
<mat-vertical-stepper [linear]="true" #stepper (selectionChange)="stepSelectionChanged($event)">
<mat-step [stepControl]="passwordFormGroup" [editable]="flgEditable">
<form [formGroup]="passwordFormGroup" fxLayout="column" fxLayoutAlign="space-between" class="my-1 pr-1">
<ng-template matStepLabel>{{passwordFormLabel}}</ng-template>
<div fxLayout="row">
<mat-form-field fxFlex="100">
<input autoFocus matInput placeholder="Password" type="password" tabindex="1" formControlName="password" required>
<mat-error *ngIf="passwordFormGroup.controls.password.errors?.required">Password is required.</mat-error>
</mat-form-field>
</div>
<div class="mt-2" fxLayout="row">
<button mat-stroked-button color="primary" tabindex="3" type="default" (click)="onAuthenticate()">Confirm</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="sendFundFormGroup" [editable]="flgEditable">
<form [formGroup]="sendFundFormGroup" fxLayout="column" class="my-1 pr-1">
<ng-template matStepLabel disabled="true">{{sendFundFormLabel}}</ng-template>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch">
<mat-form-field fxFlex="100">
<input matInput formControlName="transactionAddress" placeholder="Bitcoin Address" tabindex="4" name="address" required>
<mat-error *ngIf="sendFundFormGroup.controls.transactionAddress.errors?.required">Bitcoin address is required.</mat-error>
</mat-form-field>
<div fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<mat-form-field fxFlex.gt-sm="48" fxLayoutAlign="start end">
<mat-select tabindex="5" placeholder="Fee Rate" formControlName="transactionFeeRate">
<mat-option *ngFor="let feeRateType of feeRateTypes" [value]="feeRateType.feeRateId">
{{feeRateType.feeRateType}}
</mat-option>
</mat-select>
<mat-error *ngIf="sendFundFormGroup.controls.transactionFeeRate.errors?.required">Fees Rate is required.</mat-error>
</mat-form-field>
<div fxFlex.gt-sm="48" fxLayout="row" fxLayoutAlign="start center">
<mat-checkbox fxFlex="2" tabindex="6" color="primary" formControlName="flgMinConf" name="flgMinCon" class="mr-2"></mat-checkbox>
<mat-form-field fxFlex="98">
<input matInput formControlName="transactionBlocks" placeholder="Min Confirmation Blocks" type="number" name="blocks" step="1" min="0" tabindex="7" required>
<mat-error *ngIf="sendFundFormGroup.controls.transactionBlocks.errors?.required">Min confirmation blocks is required.</mat-error>
</mat-form-field>
</div>
</div>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button mat-stroked-button color="primary" tabindex="8" type="default" matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="confirmFormGroup">
<form [formGroup]="confirmFormGroup" fxLayout="column" fxLayoutAlign="start" class="my-1 pr-1">
<ng-template matStepLabel>{{confirmFormLabel}}</ng-template>
<div fxLayout="column">
<div fxFlex="100" class="w-100 alert alert-warn">
<fa-icon [icon]="faExclamationTriangle" class="mt-1 mr-1 alert-icon"></fa-icon>
<span>You are about to sweep all funds from RTL. Are you sure?</span>
</div>
<div fxFlex="100" class="alert alert-danger mt-1" *ngIf="sendFundError !== ''">
<fa-icon [icon]="faExclamationTriangle" class="mr-1 alert-icon"></fa-icon>
<span *ngIf="sendFundError !== ''">{{sendFundError}}</span>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button mat-stroked-button color="primary" tabindex="9" type="button" (click)="onSendFunds()">Sweep All Funds</button>
</div>
</div>
</form>
</mat-step>
</mat-vertical-stepper>
<div fxLayout="row" fxFlex="100" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" tabindex="12" type="button" [mat-dialog-close]="false" default>{{flgValidated ? 'Close' : 'Cancel'}}</button>
</div>
</div>
</ng-template>

@ -1,22 +1,29 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DecimalPipe } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { Subject, combineLatest } from 'rxjs'; import { Subject, combineLatest } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators'; import { takeUntil, filter, take } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects'; import { Actions } from '@ngrx/effects';
import { MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatVerticalStepper } from '@angular/material/stepper';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { SelNodeChild, GetInfoRoot } from '../../../shared/models/RTLconfig'; import { SelNodeChild, GetInfoRoot } from '../../../shared/models/RTLconfig';
import { CLOnChainSendFunds } from '../../../shared/models/alertData';
import { GetInfo, Balance, OnChain, Transaction } from '../../../shared/models/clModels'; import { GetInfo, Balance, OnChain, Transaction } from '../../../shared/models/clModels';
import { CURRENCY_UNITS, CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, ADDRESS_TYPES, FEE_RATE_TYPES } from '../../../shared/services/consts-enums-functions'; import { CURRENCY_UNITS, CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, ADDRESS_TYPES, FEE_RATE_TYPES } from '../../../shared/services/consts-enums-functions';
import { RTLConfiguration } from '../../../shared/models/RTLconfig'; import { RTLConfiguration } from '../../../shared/models/RTLconfig';
import { CommonService } from '../../../shared/services/common.service'; import { CommonService } from '../../../shared/services/common.service';
import { LoggerService } from '../../../shared/services/logger.service'; import { LoggerService } from '../../../shared/services/logger.service';
import { RTLEffects } from '../../../store/rtl.effects';
import * as CLActions from '../../store/cl.actions'; import * as CLActions from '../../store/cl.actions';
import * as RTLActions from '../../../store/rtl.actions'; import * as RTLActions from '../../../store/rtl.actions';
import * as fromRTLReducer from '../../../store/rtl.reducers'; import * as fromRTLReducer from '../../../store/rtl.reducers';
import * as sha256 from 'sha256';
@Component({ @Component({
selector: 'rtl-cl-on-chain-send', selector: 'rtl-cl-on-chain-send',
@ -24,8 +31,11 @@ import * as fromRTLReducer from '../../../store/rtl.reducers';
styleUrls: ['./on-chain-send.component.scss'] styleUrls: ['./on-chain-send.component.scss']
}) })
export class CLOnChainSendComponent implements OnInit, OnDestroy { export class CLOnChainSendComponent implements OnInit, OnDestroy {
@ViewChild('form', { static: false }) form: any; @ViewChild('form', { static: false }) form: any;
@ViewChild('formSweepAll', { static: false }) formSweepAll: any;
@ViewChild('stepper', { static: false }) stepper: MatVerticalStepper;
public faExclamationTriangle = faExclamationTriangle; public faExclamationTriangle = faExclamationTriangle;
public sweepAll = false;
public selNode: SelNodeChild = {}; public selNode: SelNodeChild = {};
public appConfig: RTLConfiguration; public appConfig: RTLConfiguration;
public nodeData: GetInfoRoot; public nodeData: GetInfoRoot;
@ -51,11 +61,47 @@ export class CLOnChainSendComponent implements OnInit, OnDestroy {
public unitConversionValue = 0; public unitConversionValue = 0;
public currencyUnitFormats = CURRENCY_UNIT_FORMATS; public currencyUnitFormats = CURRENCY_UNIT_FORMATS;
public advancedTitle = 'Advanced Options'; public advancedTitle = 'Advanced Options';
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; public flgValidated = false;
public flgEditable = true;
public passwordFormLabel = 'Authenticate with your RTL password';
public sendFundFormLabel = 'Sweep funds';
public confirmFormLabel = 'Confirm sweep';
passwordFormGroup: FormGroup;
sendFundFormGroup: FormGroup;
confirmFormGroup: FormGroup;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<CLOnChainSendComponent>, private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private commonService: CommonService, private decimalPipe: DecimalPipe, private actions$: Actions) {} constructor(public dialogRef: MatDialogRef<CLOnChainSendComponent>, @Inject(MAT_DIALOG_DATA) public data: CLOnChainSendFunds, private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private commonService: CommonService, private decimalPipe: DecimalPipe, private actions$: Actions, private formBuilder: FormBuilder, private rtlEffects: RTLEffects, private snackBar: MatSnackBar) {}
ngOnInit() { ngOnInit() {
this.sweepAll = this.data.sweepAll;
this.passwordFormGroup = this.formBuilder.group({
hiddenPassword: ['', [Validators.required]],
password: ['', [Validators.required]]
});
this.sendFundFormGroup = this.formBuilder.group({
transactionAddress: ['', Validators.required],
transactionFeeRate: [null],
flgMinConf: [false],
transactionBlocks: [{value: null, disabled: true}]
});
this.confirmFormGroup = this.formBuilder.group({});
this.sendFundFormGroup.controls.flgMinConf.valueChanges.pipe(takeUntil(this.unSubs[4])).subscribe(flg => {
if (flg) {
this.sendFundFormGroup.controls.transactionBlocks.enable();
this.sendFundFormGroup.controls.transactionBlocks.setValidators([Validators.required]);
this.sendFundFormGroup.controls.transactionBlocks.setValue(null);
this.sendFundFormGroup.controls.transactionFeeRate.disable();
this.sendFundFormGroup.controls.transactionFeeRate.setValue(null);
} else {
this.sendFundFormGroup.controls.transactionBlocks.disable();
this.sendFundFormGroup.controls.transactionBlocks.setValidators(null);
this.sendFundFormGroup.controls.transactionBlocks.setValue(null);
this.sendFundFormGroup.controls.transactionBlocks.setErrors(null);
this.sendFundFormGroup.controls.transactionFeeRate.enable();
this.sendFundFormGroup.controls.transactionFeeRate.setValue(null);
}
});
combineLatest( combineLatest(
this.store.select('root'), this.store.select('root'),
this.store.select('cl')) this.store.select('cl'))
@ -87,6 +133,23 @@ export class CLOnChainSendComponent implements OnInit, OnDestroy {
} }
onAuthenticate() {
if (!this.passwordFormGroup.controls.password.value) { return true; }
this.flgValidated = false;
this.store.dispatch(new RTLActions.IsAuthorized(sha256(this.passwordFormGroup.controls.password.value)));
this.rtlEffects.isAuthorizedRes
.pipe(take(1))
.subscribe(authRes => {
if (authRes !== 'ERROR') {
this.passwordFormGroup.controls.hiddenPassword.setValue(this.passwordFormGroup.controls.password.value);
this.stepper.next();
} else {
this.dialogRef.close();
this.snackBar.open('Unauthorized User. Logging out from RTL.');
}
});
}
onSendFunds() { onSendFunds() {
if(this.invalidValues) { return true; } if(this.invalidValues) { return true; }
this.sendFundError = ''; this.sendFundError = '';
@ -98,23 +161,45 @@ export class CLOnChainSendComponent implements OnInit, OnDestroy {
this.selUTXOs.forEach(utxo => this.transaction.utxos.push(utxo.txid + ':' + utxo.output)); this.selUTXOs.forEach(utxo => this.transaction.utxos.push(utxo.txid + ':' + utxo.output));
} }
this.store.dispatch(new RTLActions.OpenSpinner('Sending Funds...')); this.store.dispatch(new RTLActions.OpenSpinner('Sending Funds...'));
if(this.transaction.satoshis && this.selAmountUnit !== CurrencyUnitEnum.SATS) { if (this.sweepAll) {
this.commonService.convertCurrency(+this.transaction.satoshis, this.selAmountUnit === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : this.selAmountUnit, this.amountUnits[2], this.fiatConversion) this.transaction.satoshis = 'all';
.pipe(takeUntil(this.unSubs[2])) this.transaction.address = this.sendFundFormGroup.controls.transactionAddress.value;
.subscribe(data => { if (this.sendFundFormGroup.controls.flgMinConf.value) {
this.transaction.satoshis = data[CurrencyUnitEnum.SATS]; delete this.transaction.feeRate;
this.selAmountUnit = CurrencyUnitEnum.SATS; this.transaction.minconf = this.sendFundFormGroup.controls.transactionBlocks.value;
this.store.dispatch(new CLActions.SetChannelTransaction(this.transaction)); } else {
}); delete this.transaction.minconf;
} else { if (this.sendFundFormGroup.controls.transactionFeeRate.value) {
this.transaction.feeRate = this.sendFundFormGroup.controls.transactionFeeRate.value;
} else {
delete this.transaction.feeRate;
}
}
delete this.transaction.utxos;
this.store.dispatch(new CLActions.SetChannelTransaction(this.transaction)); this.store.dispatch(new CLActions.SetChannelTransaction(this.transaction));
} else {
if(this.transaction.satoshis && this.transaction.satoshis !== 'all' && this.selAmountUnit !== CurrencyUnitEnum.SATS) {
this.commonService.convertCurrency(+this.transaction.satoshis, this.selAmountUnit === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : this.selAmountUnit, this.amountUnits[2], this.fiatConversion)
.pipe(takeUntil(this.unSubs[2]))
.subscribe(data => {
this.transaction.satoshis = data[CurrencyUnitEnum.SATS];
this.selAmountUnit = CurrencyUnitEnum.SATS;
this.store.dispatch(new CLActions.SetChannelTransaction(this.transaction));
});
} else {
this.store.dispatch(new CLActions.SetChannelTransaction(this.transaction));
}
} }
} }
get invalidValues(): boolean { get invalidValues(): boolean {
return (!this.transaction.address || this.transaction.address === '') if (this.sweepAll) {
return (!this.sendFundFormGroup.controls.transactionAddress.value || this.sendFundFormGroup.controls.transactionAddress.value === '') || (this.sendFundFormGroup.controls.flgMinConf.value && (!this.sendFundFormGroup.controls.transactionBlocks.value || this.sendFundFormGroup.controls.transactionBlocks.value <= 0));
} else {
return (!this.transaction.address || this.transaction.address === '')
|| ((!this.transaction.satoshis || +this.transaction.satoshis <= 0)) || ((!this.transaction.satoshis || +this.transaction.satoshis <= 0))
|| (this.flgMinConf && (!this.transaction.minconf || this.transaction.minconf <= 0)); || (this.flgMinConf && (!this.transaction.minconf || this.transaction.minconf <= 0));
}
} }
resetData() { resetData() {
@ -127,6 +212,36 @@ export class CLOnChainSendComponent implements OnInit, OnDestroy {
this.selAmountUnit = CURRENCY_UNITS[0]; this.selAmountUnit = CURRENCY_UNITS[0];
} }
stepSelectionChanged(event: any) {
this.sendFundError = '';
switch (event.selectedIndex) {
case 0:
this.passwordFormLabel = 'Authenticate with your RTL password';
this.sendFundFormLabel = 'Sweep funds'
break;
case 1:
this.passwordFormLabel = 'User authenticated successfully';
this.sendFundFormLabel = 'Sweep funds'
break;
case 2:
this.passwordFormLabel = 'User authenticated successfully';
this.sendFundFormLabel = 'Sweep funds | Address: ' + this.sendFundFormGroup.controls.transactionAddress.value + (this.sendFundFormGroup.controls.flgMinConf.value ? (' | Min Confirmation Blocks: ' + this.sendFundFormGroup.controls.transactionBlocks.value) : (this.sendFundFormGroup.controls.transactionFeeRate.value ? (' | Fee Rate: ' + this.feeRateTypes.find(frType => frType.feeRateId === this.sendFundFormGroup.controls.transactionFeeRate.value).feeRateType) : ''));
break;
default:
this.passwordFormLabel = 'Authenticate with your RTL password';
this.sendFundFormLabel = 'Sweep funds'
break;
}
if (event.selectedIndex < event.previouslySelectedIndex) {
if (event.selectedIndex === 0) {
this.passwordFormGroup.controls.hiddenPassword.setValue('');
}
}
}
onUTXOSelectionChange(event: any) { onUTXOSelectionChange(event: any) {
let utxoNew = {value: 0}; let utxoNew = {value: 0};
if (this.selUTXOs.length && this.selUTXOs.length > 0) { if (this.selUTXOs.length && this.selUTXOs.length > 0) {

@ -1,7 +1,7 @@
<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="row wrap" fxLayoutAlign="start start" fxLayout.gt-sm="column" fxFlex="100" fxLayoutAlign.gt-sm="start stretch" class="padding-gap-x-large">
<div fxLayout="column" fxLayout.gt-xs="row wrap" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container"> <div fxLayout="column" fxLayout.gt-xs="row wrap" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70"> <div fxFlex="70">
<fa-icon [icon]="faHistory" class="page-title-img mr-1"></fa-icon> <fa-icon [icon]="faMoneyBillWave" class="page-title-img mr-1"></fa-icon>
<span class="page-title">UTXOs</span> <span class="page-title">UTXOs</span>
</div> </div>
<mat-form-field fxFlex="30"> <mat-form-field fxFlex="30">
@ -55,10 +55,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_transaction"> <ng-container matColumnDef="no_transaction">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!listTransactions.data || listTransactions.data.length<1">No transactions available.</p> <p *ngIf="!listTransactions?.data || listTransactions?.data?.length<1">No transactions available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_transaction']" [ngClass]="{'display-none': listTransactions.data && listTransactions.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_transaction']" [ngClass]="{'display-none': listTransactions?.data && listTransactions?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -3,7 +3,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects'; import { Actions } from '@ngrx/effects';
import { faHistory } from '@fortawesome/free-solid-svg-icons'; import { faMoneyBillWave } from '@fortawesome/free-solid-svg-icons';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator'; import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
@ -28,7 +28,7 @@ import * as fromRTLReducer from '../../../store/rtl.reducers';
export class CLOnChainTransactionHistoryComponent implements OnInit, OnDestroy { export class CLOnChainTransactionHistoryComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator; @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
faHistory = faHistory; faMoneyBillWave = faMoneyBillWave;
public displayedColumns = []; public displayedColumns = [];
public listTransactions: any; public listTransactions: any;
public flgLoading: Array<Boolean | 'error'> = [true]; public flgLoading: Array<Boolean | 'error'> = [true];

@ -23,10 +23,17 @@
<mat-tab label="Send"> <mat-tab label="Send">
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap"> <div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap">
<div fxLayout="row"> <div fxLayout="row">
<button mat-flat-button color="primary" type="button" tabindex="1" (click)="openSendFundsModal()">Send Funds</button> <button mat-flat-button color="primary" type="button" tabindex="1" (click)="openSendFundsModal(false)">Send Funds</button>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="Sweep All">
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap">
<div fxLayout="row">
<button mat-flat-button color="primary" type="button" tabindex="3" (click)="openSendFundsModal(true)">Sweep All</button>
</div>
</div>
</mat-tab>
</mat-tab-group> </mat-tab-group>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large"> <div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large">
<div fxLayout="row"> <div fxLayout="row">

@ -32,8 +32,9 @@ export class CLOnChainComponent implements OnInit, OnDestroy {
}); });
} }
openSendFundsModal() { openSendFundsModal(sweepAll: boolean) {
this.store.dispatch(new RTLActions.OpenAlert({ data: { this.store.dispatch(new RTLActions.OpenAlert({ data: {
sweepAll: sweepAll,
component: CLOnChainSendComponent component: CLOnChainSendComponent
}})); }}));
} }

@ -87,11 +87,11 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_peer"> <ng-container matColumnDef="no_peer">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="numPeers<1 && (!channels.data || channels.data.length<1)">No peers connected. Add a peer in order to open a channel.</p> <p *ngIf="numPeers<1 && (!channels?.data || channels?.data?.length<1)">No peers connected. Add a peer in order to open a channel.</p>
<p *ngIf="numPeers>0 && (!channels.data || channels.data.length<1)">No channels available.</p> <p *ngIf="numPeers>0 && (!channels?.data || channels?.data?.length<1)">No channels available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': numPeers>0 && channels.data && channels.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': numPeers>0 && channels?.data && channels?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -202,7 +202,7 @@ export class CLChannelOpenTableComponent implements OnInit, OnDestroy {
.subscribe(confirmRes => { .subscribe(confirmRes => {
if (confirmRes) { if (confirmRes) {
this.store.dispatch(new RTLActions.OpenSpinner('Closing Channel...')); this.store.dispatch(new RTLActions.OpenSpinner('Closing Channel...'));
this.store.dispatch(new CLActions.CloseChannel({channelId: channelToClose.channel_id})); this.store.dispatch(new CLActions.CloseChannel({channelId: channelToClose.channel_id, force: false}));
} }
}); });
} }

@ -52,17 +52,23 @@
</mat-select> </mat-select>
</div> </div>
</th> </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> <td mat-cell *matCellDef="let channel" fxLayoutAlign="end center" class="pr-3">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onChannelClick(channel, $event)">View Info</button> <div class="bordered-box table-actions-select">
</span></td> <mat-select placeholder="Actions" tabindex="4" class="mr-0">
<mat-select-trigger></mat-select-trigger>
<mat-option (click)="onChannelClick(channel, $event)">View Info</mat-option>
<mat-option (click)="onChannelClose(channel)" *ngIf="isCompatibleVersion && !channel.connected && channel.state === 'CHANNELD_NORMAL'">Close Channel</mat-option>
</mat-select>
</div>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="no_peer"> <ng-container matColumnDef="no_peer">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="numPeers<1 && (!channels.data || channels.data.length<1)">No peers connected. Add a peer in order to open a channel.</p> <p *ngIf="numPeers<1 && (!channels?.data || channels?.data?.length<1)">No peers connected. Add a peer in order to open a channel.</p>
<p *ngIf="numPeers>0 && (!channels.data || channels.data.length<1)">No channels available.</p> <p *ngIf="numPeers>0 && (!channels?.data || channels?.data?.length<1)">No channels available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': numPeers>0 && channels.data && channels.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': numPeers>0 && channels?.data && channels?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -2,19 +2,20 @@ import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
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';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator'; 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 { GetInfo, Channel } from '../../../../../shared/models/clModels'; import { GetInfo, Channel } from '../../../../../shared/models/clModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, ScreenSizeEnum, FEE_RATE_TYPES } from '../../../../../shared/services/consts-enums-functions'; import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, ScreenSizeEnum, FEE_RATE_TYPES, AlertTypeEnum } from '../../../../../shared/services/consts-enums-functions';
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 { CLChannelInformationComponent } from '../../channel-information-modal/channel-information.component'; import { CLChannelInformationComponent } from '../../channel-information-modal/channel-information.component';
import { CLEffects } from '../../../../store/cl.effects'; import { CLEffects } from '../../../../store/cl.effects';
import { RTLEffects } from '../../../../../store/rtl.effects'; import { RTLEffects } from '../../../../../store/rtl.effects';
import * as RTLActions from '../../../../../store/rtl.actions'; import * as RTLActions from '../../../../../store/rtl.actions';
import * as CLActions from '../../../../store/cl.actions';
import * as fromRTLReducer from '../../../../../store/rtl.reducers'; import * as fromRTLReducer from '../../../../../store/rtl.reducers';
@Component({ @Component({
@ -28,6 +29,7 @@ import * as fromRTLReducer from '../../../../../store/rtl.reducers';
export class CLChannelPendingTableComponent implements OnInit, OnDestroy { export class CLChannelPendingTableComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator; @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
public isCompatibleVersion = false;
public totalBalance = 0; public totalBalance = 0;
public displayedColumns = []; public displayedColumns = [];
public channels: any; public channels: any;
@ -72,6 +74,9 @@ export class CLChannelPendingTableComponent implements OnInit, OnDestroy {
} }
}); });
this.information = rtlStore.information; this.information = rtlStore.information;
if (this.information.api_version) {
this.isCompatibleVersion = this.commonService.isVersionCompatible(this.information.api_version, '0.4.2');
}
this.numPeers = (rtlStore.peers && rtlStore.peers.length) ? rtlStore.peers.length : 0; this.numPeers = (rtlStore.peers && rtlStore.peers.length) ? rtlStore.peers.length : 0;
this.totalBalance = rtlStore.balance.totalBalance; this.totalBalance = rtlStore.balance.totalBalance;
if (rtlStore.allChannels) { if (rtlStore.allChannels) {
@ -97,6 +102,24 @@ export class CLChannelPendingTableComponent implements OnInit, OnDestroy {
}})); }}));
} }
onChannelClose(channelToClose: Channel) {
this.store.dispatch(new RTLActions.OpenConfirmation({ data: {
type: AlertTypeEnum.CONFIRM,
alertTitle: 'Force Close Channel',
titleMessage: 'Force closing channel: ' + channelToClose.channel_id,
noBtnText: 'Cancel',
yesBtnText: 'Force Close'
}}));
this.rtlEffects.closeConfirm
.pipe(takeUntil(this.unSubs[3]))
.subscribe(confirmRes => {
if (confirmRes) {
this.store.dispatch(new RTLActions.OpenSpinner('Force Closing Channel...'));
this.store.dispatch(new CLActions.CloseChannel({channelId: channelToClose.channel_id, force: true}));
}
});
}
loadChannelsTable(mychannels) { loadChannelsTable(mychannels) {
mychannels.sort(function(a, b) { mychannels.sort(function(a, b) {
return (a.active === b.active) ? 0 : ((b.active) ? 1 : -1); return (a.active === b.active) ? 0 : ((b.active) ? 1 : -1);

@ -66,7 +66,7 @@
<div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between center"> <div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between center">
<mat-form-field fxFlex="70" fxLayoutAlign="start end"> <mat-form-field fxFlex="70" fxLayoutAlign="start end">
<input matInput [(ngModel)]="fundingAmount" placeholder="Amount" type="number" step="1000" min="1" max="{{totalBalance}}" tabindex="1" required name="amount" #amount="ngModel" [disabled]="flgUseAllBalance"> <input matInput [(ngModel)]="fundingAmount" placeholder="Amount" type="number" step="1000" min="1" max="{{totalBalance}}" tabindex="1" required name="amount" #amount="ngModel" [disabled]="flgUseAllBalance">
<mat-hint>Remaining Bal: {{totalBalance - ((fundingAmount) ? fundingAmount : 0) | number}}</mat-hint> <mat-hint>Remaining Bal: {{totalBalance - ((fundingAmount) ? fundingAmount : 0) | number}}{{flgUseAllBalance ? '. Amount replaced by UTXO balance' : ''}}</mat-hint>
<span matSuffix> {{information?.smaller_currency_unit}} </span> <span matSuffix> {{information?.smaller_currency_unit}} </span>
<mat-error *ngIf="amount.errors?.required || !fundingAmount">Amount is required.</mat-error> <mat-error *ngIf="amount.errors?.required || !fundingAmount">Amount is required.</mat-error>
<mat-error *ngIf="amount.errors?.max">Amount must be less than or equal to {{totalBalance}}.</mat-error> <mat-error *ngIf="amount.errors?.max">Amount must be less than or equal to {{totalBalance}}.</mat-error>

@ -60,10 +60,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_peer"> <ng-container matColumnDef="no_peer">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!peers.data || peers.data.length<1">No connected peers.</p> <p *ngIf="!peers?.data || peers?.data?.length<1">No connected peers.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': peers.data && peers.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': peers?.data && peers?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.pub_key === newlyAddedPeer && flgAnimate) ? 'added' : 'notAdded'"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.pub_key === newlyAddedPeer && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table> </table>

@ -60,10 +60,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_event"> <ng-container matColumnDef="no_event">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!forwardingHistoryEvents.data || forwardingHistoryEvents.data.length<1">No forwarding event available.</p> <p *ngIf="!forwardingHistoryEvents?.data || forwardingHistoryEvents?.data?.length<1">No forwarding event available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_event']" [ngClass]="{'display-none': forwardingHistoryEvents.data && forwardingHistoryEvents.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_event']" [ngClass]="{'display-none': forwardingHistoryEvents?.data && forwardingHistoryEvents?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -60,10 +60,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_event"> <ng-container matColumnDef="no_event">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!forwardingHistoryEvents.data || forwardingHistoryEvents.data.length<1">No forwarding event available.</p> <p *ngIf="!forwardingHistoryEvents?.data || forwardingHistoryEvents?.data?.length<1">No forwarding event available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_event']" [ngClass]="{'display-none': forwardingHistoryEvents.data && forwardingHistoryEvents.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_event']" [ngClass]="{'display-none': forwardingHistoryEvents?.data && forwardingHistoryEvents?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -231,7 +231,7 @@ export class SaveNewChannel implements Action {
export class CloseChannel implements Action { export class CloseChannel implements Action {
readonly type = CLOSE_CHANNEL_CL; readonly type = CLOSE_CHANNEL_CL;
constructor(public payload: {channelId: string, timeoutSec?: number}) {} constructor(public payload: {channelId: string, force: boolean}) {}
} }
export class RemoveChannel implements Action { export class RemoveChannel implements Action {

@ -13,7 +13,7 @@ import { SessionService } from '../../shared/services/session.service';
import { CommonService } from '../../shared/services/common.service'; import { CommonService } from '../../shared/services/common.service';
import { ErrorMessageComponent } from '../../shared/components/data-modal/error-message/error-message.component'; import { ErrorMessageComponent } from '../../shared/components/data-modal/error-message/error-message.component';
import { CLInvoiceInformationComponent } from '../transactions/invoice-information-modal/invoice-information.component'; import { CLInvoiceInformationComponent } from '../transactions/invoice-information-modal/invoice-information.component';
import { GetInfo, Fees, Balance, LocalRemoteBalance, Payment, FeeRates, ListInvoices, Invoice, Peer, Transaction } from '../../shared/models/clModels'; import { GetInfo, Fees, Balance, LocalRemoteBalance, Payment, FeeRates, ListInvoices, Invoice, Peer } from '../../shared/models/clModels';
import * as fromRTLReducer from '../../store/rtl.reducers'; import * as fromRTLReducer from '../../store/rtl.reducers';
import * as RTLActions from '../../store/rtl.actions'; import * as RTLActions from '../../store/rtl.actions';
@ -37,7 +37,7 @@ export class CLEffects implements OnDestroy {
this.store.select('cl') this.store.select('cl')
.pipe(takeUntil(this.unSubs[0])) .pipe(takeUntil(this.unSubs[0]))
.subscribe((rtlStore) => { .subscribe((rtlStore) => {
if(rtlStore.initialAPIResponseStatus[0] === 'INCOMPLETE' && rtlStore.initialAPIResponseStatus.length > 9) { if(rtlStore.initialAPIResponseStatus[0] === 'INCOMPLETE' && rtlStore.initialAPIResponseStatus.length > 9) { // Num of Initial APIs + 1
rtlStore.initialAPIResponseStatus[0] = 'COMPLETE'; rtlStore.initialAPIResponseStatus[0] = 'COMPLETE';
this.store.dispatch(new RTLActions.CloseSpinner()); this.store.dispatch(new RTLActions.CloseSpinner());
} }
@ -330,7 +330,7 @@ export class CLEffects implements OnDestroy {
closeChannelCL = this.actions$.pipe( closeChannelCL = this.actions$.pipe(
ofType(CLActions.CLOSE_CHANNEL_CL), ofType(CLActions.CLOSE_CHANNEL_CL),
mergeMap((action: CLActions.CloseChannel) => { mergeMap((action: CLActions.CloseChannel) => {
const queryParam = action.payload.timeoutSec ? '?unilateralTimeout =' + action.payload.timeoutSec : ''; const queryParam = action.payload.force ? '?force=' + action.payload.force : '';
return this.httpClient.delete(this.CHILD_API_URL + environment.CHANNELS_API + '/' + action.payload.channelId + queryParam) return this.httpClient.delete(this.CHILD_API_URL + environment.CHANNELS_API + '/' + action.payload.channelId + queryParam)
.pipe( .pipe(
map((postRes: any) => { map((postRes: any) => {

@ -73,10 +73,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_invoice"> <ng-container matColumnDef="no_invoice">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!invoices.data || invoices.data.length<1">No invoices available.</p> <p *ngIf="!invoices?.data || invoices?.data?.length<1">No invoices available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_invoice']" [ngClass]="{'display-none': invoices.data && invoices.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_invoice']" [ngClass]="{'display-none': invoices?.data && invoices?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.label == newlyAddedInvoiceMemo && row.value == newlyAddedInvoiceValue && flgAnimate) ? 'added' : 'notAdded'"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.label == newlyAddedInvoiceMemo && row.value == newlyAddedInvoiceValue && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table> </table>

@ -78,7 +78,7 @@
Total Attempts: {{payment?.total_parts}} Total Attempts: {{payment?.total_parts}}
</span> </span>
<ng-container *ngIf="payment.is_expanded"> <ng-container *ngIf="payment.is_expanded">
<span *ngFor="let mpp of payment?.mpps" fxLayoutAlign="start center" class="mpp-row-span"> <span *ngFor="let mpp of payment?.mpps" fxLayoutAlign="start center" class="mpp-row-span pl-3">
<span *ngIf="mpp.status === 'complete'" class="dot green mt-0" matTooltip="Completed" matTooltipPosition="right" [ngClass]="{'mr-0': screenSize === screenSizeEnum.XS}"></span> <span *ngIf="mpp.status === 'complete'" class="dot green mt-0" matTooltip="Completed" matTooltipPosition="right" [ngClass]="{'mr-0': screenSize === screenSizeEnum.XS}"></span>
<span *ngIf="mpp.status !== 'complete'" class="dot yellow mt-0" matTooltip="Incomplete/Failed" matTooltipPosition="right" [ngClass]="{'mr-0': screenSize === screenSizeEnum.XS}"></span> <span *ngIf="mpp.status !== 'complete'" class="dot yellow mt-0" matTooltip="Incomplete/Failed" matTooltipPosition="right" [ngClass]="{'mr-0': screenSize === screenSizeEnum.XS}"></span>
{{mpp.created_at_str}} {{mpp.created_at_str}}
@ -130,7 +130,7 @@
</ng-container> </ng-container>
<tr mat-row *matRowDef="let row; columns: mppColumns; when: is_group;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr> <tr mat-row *matRowDef="let row; columns: mppColumns; when: is_group;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr>
<!-- Payment Group Row End --> <!-- Payment Group Row End -->
<tr mat-footer-row *matFooterRowDef="['no_payment']" [ngClass]="{'display-none': payments.data && payments.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_payment']" [ngClass]="{'display-none': payments?.data && payments?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns; when: !is_group;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns; when: !is_group;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table> </table>

@ -25,3 +25,7 @@
.mpp-row-span { .mpp-row-span {
min-height: 4.2rem; min-height: 4.2rem;
} }
.mat-column-groupTotal {
min-width: 17rem;
}

@ -203,7 +203,7 @@ export class CLLightningPaymentsComponent implements OnInit, OnDestroy {
this.clEffects.setDecodedPaymentCL.subscribe(decodedPayment => { this.clEffects.setDecodedPaymentCL.subscribe(decodedPayment => {
this.paymentDecoded = decodedPayment; this.paymentDecoded = decodedPayment;
if(this.paymentDecoded.msatoshi) { if(this.paymentDecoded.msatoshi) {
this.commonService.convertCurrency(+this.paymentDecoded.msatoshi, CurrencyUnitEnum.SATS, this.selNode.currencyUnits[2], this.selNode.fiatConversion) this.commonService.convertCurrency(this.paymentDecoded.msatoshi ? this.paymentDecoded.msatoshi/1000 : 0, CurrencyUnitEnum.SATS, this.selNode.currencyUnits[2], this.selNode.fiatConversion)
.pipe(takeUntil(this.unSubs[1])) .pipe(takeUntil(this.unSubs[1]))
.subscribe(data => { .subscribe(data => {
if(this.selNode.fiatConversion) { if(this.selNode.fiatConversion) {

@ -101,7 +101,7 @@ export class CLLightningSendPaymentsComponent implements OnInit, OnDestroy {
this.paymentDecodedHint = 'Zero Amount Invoice | Memo: ' + this.paymentDecoded.description; this.paymentDecodedHint = 'Zero Amount Invoice | Memo: ' + this.paymentDecoded.description;
} else { } else {
this.zeroAmtInvoice = false; this.zeroAmtInvoice = false;
this.commonService.convertCurrency(+this.paymentDecoded.msatoshi, CurrencyUnitEnum.SATS, this.selNode.currencyUnits[2], this.selNode.fiatConversion) this.commonService.convertCurrency(this.paymentDecoded.msatoshi ? this.paymentDecoded.msatoshi/1000 : 0, CurrencyUnitEnum.SATS, this.selNode.currencyUnits[2], this.selNode.fiatConversion)
.pipe(takeUntil(this.unSubs[2])) .pipe(takeUntil(this.unSubs[2]))
.subscribe(data => { .subscribe(data => {
if(this.selNode.fiatConversion) { if(this.selNode.fiatConversion) {
@ -148,7 +148,7 @@ export class CLLightningSendPaymentsComponent implements OnInit, OnDestroy {
this.paymentDecodedHint = 'Zero Amount Invoice | Memo: ' + this.paymentDecoded.description; this.paymentDecodedHint = 'Zero Amount Invoice | Memo: ' + this.paymentDecoded.description;
} else { } else {
this.zeroAmtInvoice = false; this.zeroAmtInvoice = false;
this.commonService.convertCurrency(+this.paymentDecoded.msatoshi, CurrencyUnitEnum.SATS, this.selNode.currencyUnits[2], this.selNode.fiatConversion) this.commonService.convertCurrency(this.paymentDecoded.msatoshi ? this.paymentDecoded.msatoshi/1000 : 0, CurrencyUnitEnum.SATS, this.selNode.currencyUnits[2], this.selNode.fiatConversion)
.pipe(takeUntil(this.unSubs[3])) .pipe(takeUntil(this.unSubs[3]))
.subscribe(data => { .subscribe(data => {
if(this.selNode.fiatConversion) { if(this.selNode.fiatConversion) {

@ -112,7 +112,7 @@ export class ECLHomeComponent implements OnInit, OnDestroy {
if (effectsErr.action === 'FetchInfo') { if (effectsErr.action === 'FetchInfo') {
this.flgLoading[0] = 'error'; this.flgLoading[0] = 'error';
} }
if (effectsErr.action === 'FetchAudit') { if (effectsErr.action === 'FetchFees') {
this.flgLoading[1] = 'error'; this.flgLoading[1] = 'error';
} }
if (effectsErr.action === 'FetchChannels') { if (effectsErr.action === 'FetchChannels') {
@ -161,9 +161,9 @@ export class ECLHomeComponent implements OnInit, OnDestroy {
this.logger.info(rtlStore); this.logger.info(rtlStore);
}); });
this.actions$.pipe(takeUntil(this.unSubs[2]), this.actions$.pipe(takeUntil(this.unSubs[2]),
filter((action) => action.type === ECLActions.FETCH_AUDIT_ECL || action.type === ECLActions.SET_FEES_ECL)) filter((action) => action.type === ECLActions.FETCH_FEES_ECL || action.type === ECLActions.SET_FEES_ECL))
.subscribe(action => { .subscribe(action => {
if(action.type === ECLActions.FETCH_AUDIT_ECL) { if(action.type === ECLActions.FETCH_FEES_ECL) {
this.flgChildInfoUpdated = false; this.flgChildInfoUpdated = false;
} }
if(action.type === ECLActions.SET_FEES_ECL) { if(action.type === ECLActions.SET_FEES_ECL) {

@ -52,10 +52,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_transaction"> <ng-container matColumnDef="no_transaction">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!listTransactions.data || listTransactions.data.length<1">No transactions available.</p> <p *ngIf="!listTransactions?.data || listTransactions?.data?.length<1">No transactions available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_transaction']" [ngClass]="{'display-none': listTransactions.data && listTransactions.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_transaction']" [ngClass]="{'display-none': listTransactions?.data && listTransactions?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -41,10 +41,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_channel"> <ng-container matColumnDef="no_channel">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="(!channels.data || channels.data.length<1)">No inactive channels available.</p> <p *ngIf="(!channels?.data || channels?.data?.length<1)">No inactive channels available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_channel']" [ngClass]="{'display-none': channels.data && channels.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_channel']" [ngClass]="{'display-none': channels?.data && channels?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -65,11 +65,11 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_peer"> <ng-container matColumnDef="no_peer">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="numPeers<1 && (!channels.data || channels.data.length<1)">No peers connected. Add a peer in order to open a channel.</p> <p *ngIf="numPeers<1 && (!channels?.data || channels?.data?.length<1)">No peers connected. Add a peer in order to open a channel.</p>
<p *ngIf="numPeers>0 && (!channels.data || channels.data.length<1)">No open channels available.</p> <p *ngIf="numPeers>0 && (!channels?.data || channels?.data?.length<1)">No open channels available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': numPeers>0 && channels.data && channels.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': numPeers>0 && channels?.data && channels?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -130,18 +130,21 @@ export class ECLChannelOpenTableComponent implements OnInit, OnDestroy {
} }
onChannelClose(channelToClose: Channel, forceClose: boolean) { onChannelClose(channelToClose: Channel, forceClose: boolean) {
const alertTitle = (forceClose) ? 'Force Close Channel' : 'Close Channel';
const titleMessage = (forceClose) ? ('Force closing channel: ' + channelToClose.channelId) : ('Closing channel: ' + channelToClose.channelId);
const yesBtnText = (forceClose) ? 'Force Close' : 'Close Channel';
this.store.dispatch(new RTLActions.OpenConfirmation({ data: { this.store.dispatch(new RTLActions.OpenConfirmation({ data: {
type: AlertTypeEnum.CONFIRM, type: AlertTypeEnum.CONFIRM,
alertTitle: 'Close Channel', alertTitle: alertTitle,
titleMessage: 'Closing channel: ' + channelToClose.channelId, titleMessage: titleMessage,
noBtnText: 'Cancel', noBtnText: 'Cancel',
yesBtnText: 'Close Channel' yesBtnText: yesBtnText
}})); }}));
this.rtlEffects.closeConfirm this.rtlEffects.closeConfirm
.pipe(takeUntil(this.unSubs[3])) .pipe(takeUntil(this.unSubs[3]))
.subscribe(confirmRes => { .subscribe(confirmRes => {
if (confirmRes) { if (confirmRes) {
this.store.dispatch(new RTLActions.OpenSpinner('Closing Channel...')); this.store.dispatch(new RTLActions.OpenSpinner((forceClose) ? 'Force Closing Channel...' : 'Closing Channel...'));
this.store.dispatch(new ECLActions.CloseChannel({channelId: channelToClose.channelId, force: forceClose})); this.store.dispatch(new ECLActions.CloseChannel({channelId: channelToClose.channelId, force: forceClose}));
} }
}); });

@ -41,10 +41,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_channel"> <ng-container matColumnDef="no_channel">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="(!channels.data || channels.data.length<1)">No pending channels available.</p> <p *ngIf="(!channels?.data || channels?.data?.length<1)">No pending channels available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_channel']" [ngClass]="{'display-none': channels.data && channels.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_channel']" [ngClass]="{'display-none': channels?.data && channels?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -66,10 +66,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_peer"> <ng-container matColumnDef="no_peer">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!peers.data || peers.data.length<1">No connected peers.</p> <p *ngIf="!peers?.data || peers?.data?.length<1">No connected peers.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': peers.data && peers.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': peers?.data && peers?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.pub_key === newlyAddedPeer && flgAnimate) ? 'added' : 'notAdded'"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.pub_key === newlyAddedPeer && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table> </table>

@ -15,13 +15,17 @@
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Amount In (Sats)</th> <th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Amount In (Sats)</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent?.amountIn | number}}</span></td> <td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent?.amountIn | number}}</span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="amountOut"> <ng-container matColumnDef="fee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Amount Out (Sats)</th> <th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Fee Earned (Sats)</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent?.amountOut | number}}</span></td> <td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{(fhEvent?.amountIn - fhEvent?.amountOut) | number}}</span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="paymentHash"> <ng-container matColumnDef="fromAlias">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Payment Hash</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>In Channel</th>
<td mat-cell *matCellDef="let fhEvent" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '30rem'}">{{fhEvent?.paymentHash}}</td> <td mat-cell *matCellDef="let fhEvent" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '30rem'}">{{fhEvent?.fromChannelAlias}}</td>
</ng-container>
<ng-container matColumnDef="toAlias">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Out Channel</th>
<td mat-cell *matCellDef="let fhEvent" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '30rem'}">{{fhEvent?.toChannelAlias}}</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 class="px-3">
@ -38,10 +42,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_event"> <ng-container matColumnDef="no_event">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!forwardingHistoryEvents.data || forwardingHistoryEvents.data.length<1">No forwarding event available.</p> <p *ngIf="!forwardingHistoryEvents?.data || forwardingHistoryEvents?.data?.length<1">No forwarding event available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_event']" [ngClass]="{'display-none': forwardingHistoryEvents.data && forwardingHistoryEvents.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_event']" [ngClass]="{'display-none': forwardingHistoryEvents?.data && forwardingHistoryEvents?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -1,7 +1,16 @@
.mat-column-paymentHash { .mat-column-fromAlias {
padding-left: 2rem; padding-left: 2rem;
flex: 1 1 25%; flex: 1 1 20%;
width: 25%; width: 20%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mat-column-toAlias {
padding-left: 1rem;
flex: 1 1 20%;
width: 20%;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

@ -39,10 +39,10 @@ export class ECLForwardingHistoryComponent implements OnInit, OnChanges {
this.displayedColumns = ['timestamp', 'amountIn', 'actions']; this.displayedColumns = ['timestamp', 'amountIn', 'actions'];
} else if(this.screenSize === ScreenSizeEnum.SM || this.screenSize === ScreenSizeEnum.MD) { } else if(this.screenSize === ScreenSizeEnum.SM || this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false; this.flgSticky = false;
this.displayedColumns = ['timestamp', 'amountIn', 'amountOut', 'actions']; this.displayedColumns = ['timestamp', 'amountIn', 'fee', 'actions'];
} else { } else {
this.flgSticky = true; this.flgSticky = true;
this.displayedColumns = ['timestamp', 'amountIn', 'amountOut', 'paymentHash', 'actions']; this.displayedColumns = ['timestamp', 'amountIn', 'fee', 'fromAlias', 'toAlias', 'actions'];
} }
} }
@ -54,11 +54,16 @@ export class ECLForwardingHistoryComponent implements OnInit, OnChanges {
onForwardingEventClick(selFEvent: PaymentRelayed, event: any) { onForwardingEventClick(selFEvent: PaymentRelayed, event: any) {
const reorderedFHEvent = [ const reorderedFHEvent = [
[{key: 'timestampStr', value: selFEvent.timestampStr, title: 'Date/Time', width: 34, type: DataTypeEnum.DATE_TIME},
{key: 'amountIn', value: selFEvent.amountIn, title: 'Amount In (Sats)', width: 33, type: DataTypeEnum.NUMBER},
{key: 'amountOut', value: selFEvent.amountOut, title: 'Amount Out (Sats)', width: 33, type: DataTypeEnum.NUMBER}],
[{key: 'paymentHash', value: selFEvent.paymentHash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING}], [{key: 'paymentHash', value: selFEvent.paymentHash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING}],
[{key: 'timestampStr', value: selFEvent.timestampStr, title: 'Date/Time', width: 50, type: DataTypeEnum.DATE_TIME},
{key: 'fee', value: (selFEvent.amountIn - selFEvent.amountOut), title: 'Fee Earned (Sats)', width: 50, type: DataTypeEnum.NUMBER}],
[{key: 'amountIn', value: selFEvent.amountIn, title: 'Amount In (Sats)', width: 50, type: DataTypeEnum.NUMBER},
{key: 'amountOut', value: selFEvent.amountOut, title: 'Amount Out (Sats)', width: 50, type: DataTypeEnum.NUMBER}],
[{key: 'fromChannelAlias', value: selFEvent.fromChannelAlias, title: 'From Channel Alias', width: 50, type: DataTypeEnum.STRING},
{key: 'fromShortChannelId', value: selFEvent.fromShortChannelId, title: 'From Short Channel ID', width: 50, type: DataTypeEnum.STRING}],
[{key: 'fromChannelId', value: selFEvent.fromChannelId, title: 'From Channel Id', width: 100, type: DataTypeEnum.STRING}], [{key: 'fromChannelId', value: selFEvent.fromChannelId, title: 'From Channel Id', width: 100, type: DataTypeEnum.STRING}],
[{key: 'toChannelAlias', value: selFEvent.toChannelAlias, title: 'To Channel Alias', width: 50, type: DataTypeEnum.STRING},
{key: 'toShortChannelId', value: selFEvent.toShortChannelId, title: 'To Short Channel ID', width: 50, type: DataTypeEnum.STRING}],
[{key: 'toChannelId', value: selFEvent.toChannelId, title: 'To Channel Id', width: 100, type: DataTypeEnum.STRING}] [{key: 'toChannelId', value: selFEvent.toChannelId, title: 'To Channel Id', width: 100, type: DataTypeEnum.STRING}]
]; ];
this.store.dispatch(new RTLActions.OpenAlert({ data: { this.store.dispatch(new RTLActions.OpenAlert({ data: {

@ -29,7 +29,7 @@ export class ECLRoutingComponent implements OnInit, OnDestroy {
.subscribe((rtlStore) => { .subscribe((rtlStore) => {
this.errorMessage = ''; this.errorMessage = '';
rtlStore.effectErrors.forEach(effectsErr => { rtlStore.effectErrors.forEach(effectsErr => {
if (effectsErr.action === 'FetchAudit') { if (effectsErr.action === 'FetchPayments') {
this.flgLoading[0] = 'error'; this.flgLoading[0] = 'error';
this.errorMessage = (typeof(effectsErr.message) === 'object') ? JSON.stringify(effectsErr.message) : effectsErr.message; this.errorMessage = (typeof(effectsErr.message) === 'object') ? JSON.stringify(effectsErr.message) : effectsErr.message;
} }

@ -10,7 +10,7 @@ export const EFFECT_ERROR_ECL = 'EFFECT_ERROR_ECL';
export const SET_CHILD_NODE_SETTINGS_ECL = 'SET_CHILD_NODE_SETTINGS_ECL'; export const SET_CHILD_NODE_SETTINGS_ECL = 'SET_CHILD_NODE_SETTINGS_ECL';
export const FETCH_INFO_ECL = 'FETCH_INFO_ECL'; export const FETCH_INFO_ECL = 'FETCH_INFO_ECL';
export const SET_INFO_ECL = 'SET_INFO_ECL'; export const SET_INFO_ECL = 'SET_INFO_ECL';
export const FETCH_AUDIT_ECL = 'FETCH_AUDIT_ECL'; export const FETCH_FEES_ECL = 'FETCH_FEES_ECL';
export const SET_FEES_ECL = 'SET_FEES_ECL'; export const SET_FEES_ECL = 'SET_FEES_ECL';
export const FETCH_CHANNELS_ECL = 'FETCH_CHANNELS_ECL'; export const FETCH_CHANNELS_ECL = 'FETCH_CHANNELS_ECL';
export const SET_ACTIVE_CHANNELS_ECL = 'SET_ACTIVE_CHANNELS_ECL'; export const SET_ACTIVE_CHANNELS_ECL = 'SET_ACTIVE_CHANNELS_ECL';
@ -36,11 +36,10 @@ export const SAVE_NEW_CHANNEL_ECL = 'SAVE_NEW_CHANNEL_ECL';
export const UPDATE_CHANNELS_ECL = 'UPDATE_CHANNELS_ECL'; export const UPDATE_CHANNELS_ECL = 'UPDATE_CHANNELS_ECL';
export const CLOSE_CHANNEL_ECL = 'CLOSE_CHANNEL_ECL'; export const CLOSE_CHANNEL_ECL = 'CLOSE_CHANNEL_ECL';
export const REMOVE_CHANNEL_ECL = 'REMOVE_CHANNEL_ECL'; export const REMOVE_CHANNEL_ECL = 'REMOVE_CHANNEL_ECL';
export const FETCH_PAYMENTS_ECL = 'FETCH_PAYMENTS_ECL';
export const SET_PAYMENTS_ECL = 'SET_PAYMENTS_ECL'; export const SET_PAYMENTS_ECL = 'SET_PAYMENTS_ECL';
export const GET_QUERY_ROUTES_ECL = 'GET_QUERY_ROUTES_ECL'; export const GET_QUERY_ROUTES_ECL = 'GET_QUERY_ROUTES_ECL';
export const SET_QUERY_ROUTES_ECL = 'SET_QUERY_ROUTES_ECL'; export const SET_QUERY_ROUTES_ECL = 'SET_QUERY_ROUTES_ECL';
export const DECODE_PAYMENT_ECL = 'DECODE_PAYMENT_ECL';
export const SET_DECODED_PAYMENT_ECL = 'SET_DECODED_PAYMENT_ECL';
export const SEND_PAYMENT_ECL = 'SEND_PAYMENT_ECL'; export const SEND_PAYMENT_ECL = 'SEND_PAYMENT_ECL';
export const SEND_PAYMENT_STATUS_ECL = 'SEND_PAYMENT_STATUS_ECL'; export const SEND_PAYMENT_STATUS_ECL = 'SEND_PAYMENT_STATUS_ECL';
export const FETCH_TRANSACTIONS_ECL = 'FETCH_TRANSACTIONS_ECL'; export const FETCH_TRANSACTIONS_ECL = 'FETCH_TRANSACTIONS_ECL';
@ -85,8 +84,8 @@ export class SetInfo implements Action {
constructor(public payload: GetInfo) {} constructor(public payload: GetInfo) {}
} }
export class FetchAudit implements Action { export class FetchFees implements Action {
readonly type = FETCH_AUDIT_ECL; readonly type = FETCH_FEES_ECL;
} }
export class SetFees implements Action { export class SetFees implements Action {
@ -96,6 +95,7 @@ export class SetFees implements Action {
export class FetchChannels implements Action { export class FetchChannels implements Action {
readonly type = FETCH_CHANNELS_ECL; readonly type = FETCH_CHANNELS_ECL;
constructor(public payload: {fetchPayments: boolean}) {}
} }
export class SetActiveChannels implements Action { export class SetActiveChannels implements Action {
@ -204,6 +204,10 @@ export class RemoveChannel implements Action {
constructor(public payload: {channelId: string}) {} constructor(public payload: {channelId: string}) {}
} }
export class FetchPayments implements Action {
readonly type = FETCH_PAYMENTS_ECL;
}
export class SetPayments implements Action { export class SetPayments implements Action {
readonly type = SET_PAYMENTS_ECL; readonly type = SET_PAYMENTS_ECL;
constructor(public payload: Payments) {} constructor(public payload: Payments) {}
@ -219,16 +223,6 @@ export class SetQueryRoutes implements Action {
constructor(public payload: Route[]) {} constructor(public payload: Route[]) {}
} }
export class DecodePayment implements Action {
readonly type = DECODE_PAYMENT_ECL;
constructor(public payload: {routeParam: string, fromDialog: boolean}) {} // payload = routeParam
}
export class SetDecodedPayment implements Action {
readonly type = SET_DECODED_PAYMENT_ECL;
constructor(public payload: PayRequest) {}
}
export class SendPayment implements Action { export class SendPayment implements Action {
readonly type = SEND_PAYMENT_ECL; readonly type = SEND_PAYMENT_ECL;
constructor(public payload: { fromDialog: boolean, invoice: string, amountMsat?: number }) {} constructor(public payload: { fromDialog: boolean, invoice: string, amountMsat?: number }) {}
@ -288,7 +282,7 @@ export class SetLookup implements Action {
} }
export type ECLActions = ResetECLStore | ClearEffectError | EffectError | SetChildNodeSettings | export type ECLActions = ResetECLStore | ClearEffectError | EffectError | SetChildNodeSettings |
FetchInfo | SetInfo | FetchAudit | SetFees | FetchInfo | SetInfo | FetchFees | SetFees |
FetchChannels | SetActiveChannels | SetPendingChannels | SetInactiveChannels | FetchChannels | SetActiveChannels | SetPendingChannels | SetInactiveChannels |
FetchPeers | SetPeers | AddPeer | DisconnectPeer | SaveNewPeer | RemovePeer | NewlyAddedPeer | FetchPeers | SetPeers | AddPeer | DisconnectPeer | SaveNewPeer | RemovePeer | NewlyAddedPeer |
SetChannelsStatus | FetchChannelStats | SetChannelStats | SetChannelsStatus | FetchChannelStats | SetChannelStats |
@ -296,5 +290,5 @@ export type ECLActions = ResetECLStore | ClearEffectError | EffectError | SetChi
SendOnchainFunds | SendOnchainFundsRes | FetchTransactions | SetTransactions | SendOnchainFunds | SendOnchainFundsRes | FetchTransactions | SetTransactions |
SetLightningBalance | FetchPeers | SetPeers | PeerLookup | SetLookup | SetLightningBalance | FetchPeers | SetPeers | PeerLookup | SetLookup |
SaveNewChannel | UpdateChannels | CloseChannel | RemoveChannel | SaveNewChannel | UpdateChannels | CloseChannel | RemoveChannel |
SetPayments | DecodePayment | SetDecodedPayment | SendPayment | SendPaymentStatus | FetchPayments | SetPayments | SendPayment | SendPaymentStatus |
FetchInvoices | SetInvoices | CreateInvoice | AddInvoice; FetchInvoices | SetInvoices | CreateInvoice | AddInvoice;

@ -1,5 +1,5 @@
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Actions, Effect, ofType } from '@ngrx/effects'; import { Actions, Effect, ofType } from '@ngrx/effects';
@ -36,7 +36,7 @@ export class ECLEffects implements OnDestroy {
this.store.select('ecl') this.store.select('ecl')
.pipe(takeUntil(this.unSubs[0])) .pipe(takeUntil(this.unSubs[0]))
.subscribe((rtlStore) => { .subscribe((rtlStore) => {
if(rtlStore.initialAPIResponseStatus[0] === 'INCOMPLETE' && rtlStore.initialAPIResponseStatus.length > 5) { if(rtlStore.initialAPIResponseStatus[0] === 'INCOMPLETE' && rtlStore.initialAPIResponseStatus.length > 4) { // Num of Initial APIs + 1
rtlStore.initialAPIResponseStatus[0] = 'COMPLETE'; rtlStore.initialAPIResponseStatus[0] = 'COMPLETE';
this.store.dispatch(new RTLActions.CloseSpinner()); this.store.dispatch(new RTLActions.CloseSpinner());
} }
@ -72,23 +72,44 @@ export class ECLEffects implements OnDestroy {
)); ));
@Effect() @Effect()
fetchAudit = this.actions$.pipe( fetchFees = this.actions$.pipe(
ofType(ECLActions.FETCH_AUDIT_ECL), ofType(ECLActions.FETCH_FEES_ECL),
mergeMap((action: ECLActions.FetchAudit) => { mergeMap((action: ECLActions.FetchFees) => {
this.store.dispatch(new ECLActions.ClearEffectError('FetchAudit')); this.store.dispatch(new ECLActions.ClearEffectError('FetchFees'));
return this.httpClient.get<Audit>(this.CHILD_API_URL + environment.FEES_API); return this.httpClient.get<Audit>(this.CHILD_API_URL + environment.FEES_API + '/fees')
}), .pipe(map((fees: any) => {
map((audit: Audit) => { this.logger.info(fees);
this.logger.info(audit); return {
this.store.dispatch(new ECLActions.SetPayments(audit.payments)); type: ECLActions.SET_FEES_ECL,
return { payload: fees ? fees : {}
type: ECLActions.SET_FEES_ECL, };
payload: audit && audit.fees ? audit.fees : {} },
}; catchError((err: any) => {
}), this.handleErrorWithoutAlert('FetchFees', 'Fetching Fees Failed.', err);
catchError((err: any) => { return of({type: RTLActions.VOID});
this.handleErrorWithoutAlert('FetchAudit', 'Fetching Fees Failed.', err); })
return of({type: RTLActions.VOID}); ));
}
));
@Effect()
fetchPayments = this.actions$.pipe(
ofType(ECLActions.FETCH_PAYMENTS_ECL),
mergeMap((action: ECLActions.FetchPayments) => {
this.store.dispatch(new ECLActions.ClearEffectError('FetchPayments'));
return this.httpClient.get<Audit>(this.CHILD_API_URL + environment.FEES_API + '/payments')
.pipe(map((payments: any) => {
this.logger.info(payments);
return {
type: ECLActions.SET_PAYMENTS_ECL,
payload: payments ? payments : {}
};
},
catchError((err: any) => {
this.handleErrorWithoutAlert('FetchPayments', 'Fetching Payments Failed.', err);
return of({type: RTLActions.VOID});
})
));
} }
)); ));
@ -104,6 +125,9 @@ export class ECLEffects implements OnDestroy {
this.store.dispatch(new ECLActions.SetPendingChannels((channelsRes && channelsRes.pendingChannels.length > 0) ? channelsRes.pendingChannels : [])); this.store.dispatch(new ECLActions.SetPendingChannels((channelsRes && channelsRes.pendingChannels.length > 0) ? channelsRes.pendingChannels : []));
this.store.dispatch(new ECLActions.SetInactiveChannels((channelsRes && channelsRes.inactiveChannels.length > 0) ? channelsRes.inactiveChannels : [])); this.store.dispatch(new ECLActions.SetInactiveChannels((channelsRes && channelsRes.inactiveChannels.length > 0) ? channelsRes.inactiveChannels : []));
this.store.dispatch(new ECLActions.SetLightningBalance(channelsRes.lightningBalances)); this.store.dispatch(new ECLActions.SetLightningBalance(channelsRes.lightningBalances));
if (action.payload.fetchPayments) {
this.store.dispatch(new ECLActions.FetchPayments());
}
return { return {
type: ECLActions.SET_CHANNELS_STATUS_ECL, type: ECLActions.SET_CHANNELS_STATUS_ECL,
payload: channelsRes.channelStatus payload: channelsRes.channelStatus
@ -220,7 +244,7 @@ export class ECLEffects implements OnDestroy {
map((postRes: Peer[]) => { map((postRes: Peer[]) => {
this.logger.info(postRes); this.logger.info(postRes);
this.store.dispatch(new RTLActions.CloseSpinner()); this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new ECLActions.SetPeers((postRes && postRes.length) ? (postRes.filter(peer => peer.state !== 'DISCONNECTED')) : [])); this.store.dispatch(new ECLActions.SetPeers((postRes && postRes.length) ? postRes : []));
return { return {
type: ECLActions.NEWLY_ADDED_PEER_ECL, type: ECLActions.NEWLY_ADDED_PEER_ECL,
payload: { peer: postRes[0] } payload: { peer: postRes[0] }
@ -327,7 +351,7 @@ export class ECLEffects implements OnDestroy {
this.logger.info(postRes); this.logger.info(postRes);
setTimeout(() => { setTimeout(() => {
this.store.dispatch(new RTLActions.CloseSpinner()); this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new ECLActions.FetchChannels()); this.store.dispatch(new ECLActions.FetchChannels({fetchPayments: false}));
this.store.dispatch(new RTLActions.OpenSnackBar('Channel Closed Successfully!')); this.store.dispatch(new RTLActions.OpenSnackBar('Channel Closed Successfully!'));
}, 2000); }, 2000);
return { return {
@ -372,42 +396,6 @@ export class ECLEffects implements OnDestroy {
}) })
); );
@Effect()
decodePayment = this.actions$.pipe(
ofType(ECLActions.DECODE_PAYMENT_ECL),
mergeMap((action: ECLActions.DecodePayment) => {
this.store.dispatch(new ECLActions.ClearEffectError('DecodePayment'));
return this.httpClient.get(this.CHILD_API_URL + environment.PAYMENTS_API + '/' + action.payload.routeParam)
.pipe(
map((decodedPayment) => {
this.logger.info(decodedPayment);
this.store.dispatch(new RTLActions.CloseSpinner());
return {
type: ECLActions.SET_DECODED_PAYMENT_ECL,
payload: decodedPayment ? decodedPayment : {}
};
}),
catchError((err: any) => {
if (action.payload.fromDialog) {
this.handleErrorWithoutAlert('DecodePayment', 'Decode Payment Failed.', err);
} else {
this.handleErrorWithAlert('ERROR', 'Decode Payment Failed', this.CHILD_API_URL + environment.PAYMENTS_API + '/' + action.payload.routeParam, err);
}
return of({type: RTLActions.VOID});
})
);
})
);
@Effect({ dispatch: false })
setDecodedPayment = this.actions$.pipe(
ofType(ECLActions.SET_DECODED_PAYMENT_ECL),
map((action: ECLActions.SetDecodedPayment) => {
this.logger.info(action.payload);
return action.payload;
})
);
@Effect() @Effect()
sendPayment = this.actions$.pipe( sendPayment = this.actions$.pipe(
ofType(ECLActions.SEND_PAYMENT_ECL), ofType(ECLActions.SEND_PAYMENT_ECL),
@ -432,9 +420,8 @@ export class ECLEffects implements OnDestroy {
setTimeout(() => { setTimeout(() => {
this.store.dispatch(new ECLActions.SendPaymentStatus(sendRes)); this.store.dispatch(new ECLActions.SendPaymentStatus(sendRes));
this.store.dispatch(new RTLActions.CloseSpinner()); this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new ECLActions.FetchChannels()); this.store.dispatch(new ECLActions.FetchChannels({fetchPayments: true}));
this.store.dispatch(new ECLActions.SetDecodedPayment({})); this.store.dispatch(new ECLActions.FetchPayments());
this.store.dispatch(new ECLActions.FetchAudit());
this.store.dispatch(new RTLActions.OpenSnackBar('Payment Submitted!')); this.store.dispatch(new RTLActions.OpenSnackBar('Payment Submitted!'));
}, 3000); }, 3000);
return { type: RTLActions.VOID }; return { type: RTLActions.VOID };
@ -597,8 +584,8 @@ export class ECLEffects implements OnDestroy {
}; };
this.store.dispatch(new RTLActions.OpenSpinner('Initializing Node Data...')); this.store.dispatch(new RTLActions.OpenSpinner('Initializing Node Data...'));
this.store.dispatch(new RTLActions.SetNodeData(node_data)); this.store.dispatch(new RTLActions.SetNodeData(node_data));
this.store.dispatch(new ECLActions.FetchAudit()); this.store.dispatch(new ECLActions.FetchChannels({fetchPayments: true}));
this.store.dispatch(new ECLActions.FetchChannels()); this.store.dispatch(new ECLActions.FetchFees());
this.store.dispatch(new ECLActions.FetchOnchainBalance()); this.store.dispatch(new ECLActions.FetchOnchainBalance());
this.store.dispatch(new ECLActions.FetchPeers()); this.store.dispatch(new ECLActions.FetchPeers());
let newRoute = this.location.path(); let newRoute = this.location.path();

@ -156,7 +156,30 @@ export function ECLReducer(state = initECLState, action: ECLActions.ECLActions)
activeChannels: modifiedChannels activeChannels: modifiedChannels
}; };
case ECLActions.SET_PAYMENTS_ECL: case ECLActions.SET_PAYMENTS_ECL:
newAPIStatus = [...state.initialAPIResponseStatus, 'PAYMENTS']; if (action.payload && action.payload.relayed) {
const storedChannels = [...state.activeChannels, ...state.pendingChannels, ...state.inactiveChannels];
action.payload.relayed.forEach(event => {
if (storedChannels && storedChannels.length > 0) {
for (let idx = 0; idx < storedChannels.length; idx++) {
if (storedChannels[idx].channelId.toString() === event.fromChannelId) {
event.fromChannelAlias = storedChannels[idx].alias ? storedChannels[idx].alias : event.fromChannelId;
event.fromShortChannelId = storedChannels[idx].shortChannelId ? storedChannels[idx].shortChannelId : '';
if (event.toChannelAlias) { return; }
}
if (storedChannels[idx].channelId.toString() === event.toChannelId) {
event.toChannelAlias = storedChannels[idx].alias ? storedChannels[idx].alias : event.toChannelId;
event.toShortChannelId = storedChannels[idx].shortChannelId ? storedChannels[idx].shortChannelId : '';
if (event.fromChannelAlias) { return; }
}
}
} else {
event.fromChannelAlias = event.fromChannelId;
event.fromShortChannelId = '';
event.toChannelAlias = event.toChannelId;
event.toShortChannelId = '';
}
});
}
return { return {
...state, ...state,
initialAPIResponseStatus: newAPIStatus, initialAPIResponseStatus: newAPIStatus,

@ -16,15 +16,25 @@
<qrcode [qrdata]="invoice.serialized" [margin]="2" [width]="qrWidth" [errorCorrectionLevel]="'L'" [allowEmptyString]="true"></qrcode> <qrcode [qrdata]="invoice.serialized" [margin]="2" [width]="qrWidth" [errorCorrectionLevel]="'L'" [allowEmptyString]="true"></qrcode>
</div> </div>
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="40"> <div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">{{screenSize === screenSizeEnum.XS ? 'Amount' : 'Amount Requested'}}</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Amount Requested</h4>
<span class="foreground-secondary-text">{{(invoice.amount || 0) | number}} Sats<ng-container *ngIf="!invoice.amount"> (zero amount) </ng-container></span> <span class="foreground-secondary-text">{{(invoice.amount || 0) | number}} Sats<ng-container *ngIf="!invoice.amount"> (zero amount) </ng-container></span>
</div> </div>
<div fxFlex="40"> <div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Amount Settled</h4>
<span class="foreground-secondary-text">
<ng-container *ngIf="invoice.amountSettled">{{invoice.amountSettled | number}} Sats</ng-container>
<ng-container *ngIf="!invoice.amountSettled">-</ng-container>
</span>
</div>
</div>
<mat-divider class="w-100 my-1"></mat-divider>
<div fxLayout="row">
<div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Date Created</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Date Created</h4>
<span class="overflow-wrap foreground-secondary-text">{{invoice.timestampStr}}</span> <span class="overflow-wrap foreground-secondary-text">{{invoice.timestampStr}}</span>
</div> </div>
<div fxFlex="20"> <div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Status</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Status</h4>
<span class="overflow-wrap foreground-secondary-text">{{invoice.status | titlecase}}</span> <span class="overflow-wrap foreground-secondary-text">{{invoice.status | titlecase}}</span>
</div> </div>

@ -53,8 +53,15 @@
</ng-container> </ng-container>
<ng-container matColumnDef="amount"> <ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before" class="pr-3"> Amount (Sats) </th> <th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before" class="pr-3"> Amount (Sats) </th>
<td mat-cell *matCellDef="let invoice" class="pr-3"><span fxLayoutAlign="end center"> {{invoice.amount | number:'1.0-0'}} <td mat-cell *matCellDef="let invoice" class="pr-3">
</span></td> <span fxLayoutAlign="end center"> {{invoice.amount ? (invoice.amount | number:'1.0-0') : '-'}}</span>
</td>
</ng-container>
<ng-container matColumnDef="amountSettled">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before" class="pr-3"> Amount Settled (Sats) </th>
<td mat-cell *matCellDef="let invoice" class="pr-3">
<span fxLayoutAlign="end center"> {{invoice.amountSettled ? (invoice.amountSettled | number:'1.0-0') : '-'}}</span>
</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 class="px-3">
@ -71,10 +78,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_invoice"> <ng-container matColumnDef="no_invoice">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!invoices.data || invoices.data.length<1">No invoices available.</p> <p *ngIf="!invoices?.data || invoices?.data?.length<1">No invoices available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_invoice']" [ngClass]="{'display-none': invoices.data && invoices.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_invoice']" [ngClass]="{'display-none': invoices?.data && invoices?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.label == newlyAddedInvoiceMemo && row.value == newlyAddedInvoiceValue && flgAnimate) ? 'added' : 'notAdded'"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.label == newlyAddedInvoiceMemo && row.value == newlyAddedInvoiceValue && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table> </table>

@ -64,13 +64,13 @@ export class ECLLightningInvoicesComponent implements OnInit, OnDestroy {
this.displayedColumns = ['timestamp', 'amount', 'actions']; this.displayedColumns = ['timestamp', 'amount', 'actions'];
} else if(this.screenSize === ScreenSizeEnum.SM) { } else if(this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false; this.flgSticky = false;
this.displayedColumns = ['timestamp', 'description', 'amount', 'actions']; this.displayedColumns = ['timestamp', 'amount', 'amountSettled', 'actions'];
} else if(this.screenSize === ScreenSizeEnum.MD) { } else if(this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false; this.flgSticky = false;
this.displayedColumns = ['timestamp', 'description', 'amount', 'actions']; this.displayedColumns = ['timestamp', 'amount', 'amountSettled', 'actions'];
} else { } else {
this.flgSticky = true; this.flgSticky = true;
this.displayedColumns = ['timestamp', 'receivedAt', 'description', 'amount', 'actions']; this.displayedColumns = ['timestamp', 'receivedAt', 'description', 'amount', 'amountSettled', 'actions'];
} }
} }

@ -42,16 +42,22 @@
<mat-divider class="w-100 my-1"></mat-divider> <mat-divider class="w-100 my-1"></mat-divider>
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="100"> <div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Recipient Node ID</h4> <h4 fxLayoutAlign="start" class="font-bold-500">Recipient Node</h4>
<span class="foreground-secondary-text">{{payment.recipientNodeId}}</span> <span class="foreground-secondary-text">{{payment.recipientNodeAlias}}</span>
</div> </div>
</div> </div>
<mat-divider class="w-100 my-1"></mat-divider> <mat-divider class="w-100 my-1"></mat-divider>
<div fxLayout="row" *ngIf="description">
<div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Description</h4>
<span class="foreground-secondary-text">{{description}}</span>
</div>
</div>
<mat-divider class="w-100 my-1" *ngIf="description"></mat-divider>
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="100"> <div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Parts</h4>
<mat-accordion> <mat-accordion>
<mat-expansion-panel (opened)="onExpansionOpen(true)" (closed)="onExpansionOpen(false)" class="flat-expansion-panel my-1" *ngFor="let part of payment.parts; index as i"> <mat-expansion-panel [expanded]="expansionOpen" (opened)="onExpansionOpen(true)" (closed)="onExpansionOpen(false)" class="flat-expansion-panel my-1" *ngFor="let part of payment.parts; index as i">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<h4 fxFlex="30" fxLayoutAlign="start" class="font-bold-500">Part {{i + 1}}</h4> <h4 fxFlex="30" fxLayoutAlign="start" class="font-bold-500">Part {{i + 1}}</h4>
@ -79,8 +85,8 @@
<mat-divider class="w-100 my-1"></mat-divider> <mat-divider class="w-100 my-1"></mat-divider>
<div fxLayout="row"> <div fxLayout="row">
<div fxFlex="100"> <div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">To Channel ID</h4> <h4 fxLayoutAlign="start" class="font-bold-500">To Channel</h4>
<span class="overflow-wrap foreground-secondary-text">{{part.toChannelId}}</span> <span class="overflow-wrap foreground-secondary-text">{{part.toChannelAlias}}</span>
</div> </div>
</div> </div>
</div> </div>
@ -90,7 +96,7 @@
</div> </div>
</div> </div>
</mat-card-content> </mat-card-content>
<div *ngIf="expansionOpen || (payment.parts && payment.parts.length > 2)" fxLayout="row" fxLayoutAlign="start end" class="btn-sticky-container"> <div fxLayout="row" fxLayoutAlign="start end" class="btn-sticky-container">
<button mat-mini-fab aria-label="Scroll Down" fxLayoutAlign="center center" (click)="onScrollDown()"> <button mat-mini-fab aria-label="Scroll Down" fxLayoutAlign="center center" (click)="onScrollDown()">
<mat-icon fxLayoutAlign="center center">arrow_downward</mat-icon> <mat-icon fxLayoutAlign="center center">arrow_downward</mat-icon>
</button> </button>

@ -12,13 +12,17 @@ import { ECLPaymentInformation } from '../../../shared/models/alertData';
export class ECLPaymentInformationComponent implements OnInit, AfterViewChecked { export class ECLPaymentInformationComponent implements OnInit, AfterViewChecked {
@ViewChild('scrollContainer', { static: true }) scrollContainer: ElementRef; @ViewChild('scrollContainer', { static: true }) scrollContainer: ElementRef;
public payment: PaymentSent; public payment: PaymentSent;
public description: string = null;
public shouldScroll = true; public shouldScroll = true;
public expansionOpen = false; public expansionOpen = true;
constructor(public dialogRef: MatDialogRef<ECLPaymentInformationComponent>, @Inject(MAT_DIALOG_DATA) public data: ECLPaymentInformation) { } constructor(public dialogRef: MatDialogRef<ECLPaymentInformationComponent>, @Inject(MAT_DIALOG_DATA) public data: ECLPaymentInformation) { }
ngOnInit() { ngOnInit() {
this.payment = this.data.payment; this.payment = this.data.payment;
if (this.data.sentPaymentInfo.length > 0 && this.data.sentPaymentInfo[0].paymentRequest && this.data.sentPaymentInfo[0].paymentRequest.description && this.data.sentPaymentInfo[0].paymentRequest.description !== '') {
this.description = this.data.sentPaymentInfo[0].paymentRequest.description;
}
} }
ngAfterViewChecked() { ngAfterViewChecked() {

@ -32,11 +32,19 @@
</ng-container> </ng-container>
<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 payment" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '30rem'}">{{payment?.id}}</td> <td mat-cell *matCellDef="let payment">
<div class="ellipsis-parent" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '22rem'}">
<span class="ellipsis-child">{{payment.id}}</span>
</div>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="recipientNodeId"> <ng-container matColumnDef="recipientNodeAlias">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Destination</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Destination</th>
<td mat-cell *matCellDef="let payment" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '30rem'}">{{payment?.recipientNodeId}}</td> <td mat-cell *matCellDef="let payment">
<div class="ellipsis-parent" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '22rem'}">
<span class="ellipsis-child">{{payment.recipientNodeAlias}}</span>
</div>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="recipientAmount"> <ng-container matColumnDef="recipientAmount">
<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>
@ -52,15 +60,82 @@
</div> </div>
</th> </th>
<td mat-cell *matCellDef="let payment" class="px-3" fxLayoutAlign="end center"> <td mat-cell *matCellDef="let payment" class="px-3" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onPaymentClick(payment,$event)">View Info</button> <button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onPaymentClick(payment)">View Info</button>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="no_payment"> <ng-container matColumnDef="no_payment">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!payments.data || payments.data.length<1">No payments available.</p> <p *ngIf="!payments?.data || payments?.data?.length<1">No payments available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_payment']" [ngClass]="{'display-none': payments.data && payments.data.length>0}"></tr>
<!-- Payment Group Row Start -->
<ng-container matColumnDef="groupTotal">
<td mat-cell *matCellDef="let payment">
<span fxLayoutAlign="start center" class="part-row-span">
Total Attempts: {{payment?.parts?.length}}
</span>
<ng-container *ngIf="payment.is_expanded">
<span *ngFor="let part of payment?.parts" fxLayoutAlign="start center" class="part-row-span pl-3">
{{part.timestampStr}}
</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="groupId">
<td mat-cell *matCellDef="let payment">
<div fxLayoutAlign="start center" class="ellipsis-parent part-row-span" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '22rem'}">
<span class="ellipsis-child">{{payment.id}}</span>
</div>
<span *ngIf="payment.is_expanded">
<span *ngFor="let part of payment?.parts" fxLayoutAlign="start center" class="part-row-span">
<span fxLayoutAlign="start center" class="ellipsis-parent part-row-span" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '22rem'}">
<span class="ellipsis-child">{{part.id}}</span>
</span>
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="groupChannelAlias">
<td mat-cell *matCellDef="let payment">
<div fxLayoutAlign="start center" class="ellipsis-parent part-row-span" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '22rem'}">
<span class="ellipsis-child">{{payment?.recipientNodeAlias}}</span>
</div>
<span *ngIf="payment.is_expanded">
<span *ngFor="let part of payment?.parts" fxLayoutAlign="start center" class="part-row-span">
<span fxLayoutAlign="start center" class="ellipsis-parent part-row-span" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '22rem'}">
<span class="ellipsis-child">{{part.toChannelAlias}}</span>
</span>
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="groupAmount">
<td mat-cell *matCellDef="let payment">
<span fxLayoutAlign="end center" class="part-row-span">{{payment?.recipientAmount | number:'1.0-0'}}</span>
<span *ngIf="payment.is_expanded">
<span *ngFor="let part of payment?.parts" fxLayoutAlign="end center" class="part-row-span">
{{part.amount | number:'1.0-0'}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="groupAction">
<td mat-cell *matCellDef="let payment" class="px-3">
<span fxLayoutAlign="end start">
<button mat-flat-button class="btn-part-expand" color="primary" type="button" tabindex="5" (click)="payment.is_expanded = !payment.is_expanded">{{payment.is_expanded ? 'Hide' : 'Show'}}</button>
</span>
<div *ngIf="payment.is_expanded">
<div *ngFor="let part of payment?.parts; index as i" fxLayoutAlign="end start">
<button mat-stroked-button class="btn-part-info" color="primary" type="button" tabindex="6" (click)="onPartClick(part, payment)">View {{i + 1}}</button>
</div>
</div>
</td>
</ng-container>
<tr mat-row *matRowDef="let row; columns: partColumns; when: is_group;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr>
<!-- Payment Group Row End -->
<tr mat-footer-row *matFooterRowDef="['no_payment']" [ngClass]="{'display-none': payments?.data && payments?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table> </table>

@ -1,10 +1,36 @@
.mat-column-id, .mat-column-recipientNodeId, .mat-column-paymentHash { .mat-column-id, .mat-column-recipientNodeAlias,
flex: 1 1 20%; .mat-column-groupId, .mat-column-groupChannelAlias {
white-space: nowrap; padding: 0 1rem;
overflow: hidden; flex: 0 0 25%;
text-overflow: ellipsis; width: 25%;
& .ellipsis-parent {
display: flex;
}
} }
.mat-column-actions { .mat-column-actions {
min-height: 4.8rem; min-height: 4.8rem;
} }
.mat-column-groupAction {
min-height: 4.8rem;
& .btn-part-expand {
width: 9rem;
}
& .btn-part-info {
margin-top: 0.5rem;
width: 9rem;
}
}
.part-row-span {
min-height: 4.2rem;
place-content: center flex-start;
align-items: center;
}
.mat-column-groupTotal {
min-width: 15rem;
}

@ -1,6 +1,6 @@
import { Component, OnInit, OnDestroy, ViewChild, Input } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
import { DecimalPipe, TitleCasePipe } from '@angular/common'; import { DecimalPipe, TitleCasePipe } from '@angular/common';
import { Subject } from 'rxjs'; import { forkJoin, Subject } from 'rxjs';
import { takeUntil, take } from 'rxjs/operators'; import { takeUntil, take } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { faHistory } from '@fortawesome/free-solid-svg-icons'; import { faHistory } from '@fortawesome/free-solid-svg-icons';
@ -8,10 +8,11 @@ import { faHistory } from '@fortawesome/free-solid-svg-icons';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator'; 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 { GetInfo, PayRequest, PaymentSent } from '../../../shared/models/eclModels'; import { GetInfo, PayRequest, PaymentSent, PaymentSentPart } from '../../../shared/models/eclModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, CurrencyUnitEnum, CURRENCY_UNIT_FORMATS } from '../../../shared/services/consts-enums-functions'; import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, CurrencyUnitEnum, CURRENCY_UNIT_FORMATS } from '../../../shared/services/consts-enums-functions';
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 { DataService } from '../../../shared/services/data.service';
import { newlyAddedRowAnimation } from '../../../shared/animation/row-animation'; import { newlyAddedRowAnimation } from '../../../shared/animation/row-animation';
import { ECLLightningSendPaymentsComponent } from '../send-payment-modal/send-payment.component'; import { ECLLightningSendPaymentsComponent } from '../send-payment-modal/send-payment.component';
@ -47,6 +48,7 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
public paymentJSONArr: PaymentSent[] = []; public paymentJSONArr: PaymentSent[] = [];
public paymentDecoded: PayRequest = {}; public paymentDecoded: PayRequest = {};
public displayedColumns = []; public displayedColumns = [];
public partColumns = [];
public paymentRequest = ''; public paymentRequest = '';
public paymentDecodedHint = ''; public paymentDecodedHint = '';
public flgSticky = false; public flgSticky = false;
@ -56,20 +58,24 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
public screenSizeEnum = ScreenSizeEnum; public screenSizeEnum = ScreenSizeEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<fromRTLReducer.RTLState>, private rtlEffects: RTLEffects, private eclEffects: ECLEffects, private decimalPipe: DecimalPipe, private titleCasePipe: TitleCasePipe) { constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<fromRTLReducer.RTLState>, private rtlEffects: RTLEffects, private eclEffects: ECLEffects, private decimalPipe: DecimalPipe, private dataService: DataService) {
this.screenSize = this.commonService.getScreenSize(); this.screenSize = this.commonService.getScreenSize();
if(this.screenSize === ScreenSizeEnum.XS) { if(this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false; this.flgSticky = false;
this.displayedColumns = ['firstPartTimestamp', 'actions']; this.displayedColumns = ['firstPartTimestamp', 'actions'];
this.partColumns = ['groupTotal', 'groupAction'];
} else if(this.screenSize === ScreenSizeEnum.SM) { } else if(this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false; this.flgSticky = false;
this.displayedColumns = ['firstPartTimestamp', 'recipientAmount', 'actions']; this.displayedColumns = ['firstPartTimestamp', 'recipientAmount', 'actions'];
this.partColumns = ['groupTotal', 'groupAmount', 'groupAction'];
} else if(this.screenSize === ScreenSizeEnum.MD) { } else if(this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false; this.flgSticky = false;
this.displayedColumns = ['firstPartTimestamp', 'id', 'recipientAmount', 'actions']; this.displayedColumns = ['firstPartTimestamp', 'id', 'recipientAmount', 'actions'];
this.partColumns = ['groupTotal', 'groupId', 'groupAmount', 'groupAction'];
} else { } else {
this.flgSticky = true; this.flgSticky = true;
this.displayedColumns = ['firstPartTimestamp', 'id', 'recipientNodeId', 'recipientAmount', 'actions']; this.displayedColumns = ['firstPartTimestamp', 'id', 'recipientNodeAlias', 'recipientAmount', 'actions'];
this.partColumns = ['groupTotal', 'groupId', 'groupChannelAlias', 'groupAmount', 'groupAction'];
} }
} }
@ -78,14 +84,31 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.unSubs[0])) .pipe(takeUntil(this.unSubs[0]))
.subscribe((rtlStore) => { .subscribe((rtlStore) => {
rtlStore.effectErrors.forEach(effectsErr => { rtlStore.effectErrors.forEach(effectsErr => {
if (effectsErr.action === 'FetchAudit') { if (effectsErr.action === 'FetchPayments') {
this.flgLoading[0] = 'error'; this.flgLoading[0] = 'error';
} }
}); });
this.information = rtlStore.information; this.information = rtlStore.information;
this.selNode = rtlStore.nodeSettings; this.selNode = rtlStore.nodeSettings;
if (rtlStore.payments.sent) {
rtlStore.payments.sent.map(sentPayment => {
let peerFound = rtlStore.peers.find(peer => peer.nodeId === sentPayment.recipientNodeId);
sentPayment.recipientNodeAlias = peerFound ? peerFound.alias : sentPayment.recipientNodeId;
if (sentPayment.parts) {
sentPayment.parts.map(part => {
let channelFound = rtlStore.activeChannels.find(channel => channel.channelId === part.toChannelId);
part.toChannelAlias = channelFound ? channelFound.alias : part.toChannelId;
});
}
});
}
this.paymentJSONArr = (rtlStore.payments && rtlStore.payments.sent && rtlStore.payments.sent.length > 0) ? rtlStore.payments.sent : []; this.paymentJSONArr = (rtlStore.payments && rtlStore.payments.sent && rtlStore.payments.sent.length > 0) ? rtlStore.payments.sent : [];
this.payments = new MatTableDataSource<PaymentSent>([...this.paymentJSONArr]); this.payments = new MatTableDataSource<PaymentSent>([...this.paymentJSONArr]);
// if(this.paymentJSONArr[0] && this.paymentJSONArr[0].parts) { // FOR MPP TESTING
// this.paymentJSONArr[0].parts.push({
// id: 'ID', amount: 100, feesPaid: 0, toChannelId: 'toChannel', toChannelAlias: 'Alias', timestampStr: 'str'
// });
// }
this.payments.data = this.paymentJSONArr; this.payments.data = this.paymentJSONArr;
this.payments.sort = this.sort; this.payments.sort = this.sort;
this.payments.sortingDataAccessor = (data, sortHeaderId) => (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : +data[sortHeaderId]; this.payments.sortingDataAccessor = (data, sortHeaderId) => (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : +data[sortHeaderId];
@ -104,10 +127,8 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
if (this.paymentDecoded.timestamp) { if (this.paymentDecoded.timestamp) {
this.sendPayment(); this.sendPayment();
} else { } else {
this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...')); this.dataService.decodePayment(this.paymentRequest, false)
this.store.dispatch(new ECLActions.DecodePayment({routeParam: this.paymentRequest, fromDialog: false})); .pipe(take(1)).subscribe((decodedPayment: PayRequest) => {
this.eclEffects.setDecodedPayment.pipe(take(1))
.subscribe(decodedPayment => {
this.paymentDecoded = decodedPayment; this.paymentDecoded = decodedPayment;
if (this.paymentDecoded.timestamp) { if (this.paymentDecoded.timestamp) {
if (!this.paymentDecoded.amount) { if (!this.paymentDecoded.amount) {
@ -189,9 +210,8 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
this.paymentRequest = event; this.paymentRequest = event;
this.paymentDecodedHint = ''; this.paymentDecodedHint = '';
if(this.paymentRequest && this.paymentRequest.length > 100) { if(this.paymentRequest && this.paymentRequest.length > 100) {
this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...')); this.dataService.decodePayment(this.paymentRequest, false)
this.store.dispatch(new ECLActions.DecodePayment({routeParam: this.paymentRequest, fromDialog: false})); .pipe(take(1)).subscribe((decodedPayment: PayRequest) => {
this.eclEffects.setDecodedPayment.subscribe(decodedPayment => {
this.paymentDecoded = decodedPayment; this.paymentDecoded = decodedPayment;
if(this.paymentDecoded.amount) { if(this.paymentDecoded.amount) {
this.commonService.convertCurrency(+this.paymentDecoded.amount, CurrencyUnitEnum.SATS, this.selNode.currencyUnits[2], this.selNode.fiatConversion) this.commonService.convertCurrency(+this.paymentDecoded.amount, CurrencyUnitEnum.SATS, this.selNode.currencyUnits[2], this.selNode.fiatConversion)
@ -222,21 +242,92 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
this.form.resetForm(); this.form.resetForm();
} }
onPaymentClick(selPayment: PaymentSent, event: any) { is_group(index: number, payment: PaymentSent) {
this.store.dispatch(new RTLActions.OpenAlert({ data: { return payment.parts && payment.parts.length > 1;
}
onPaymentClick(selPayment: PaymentSent) {
if (selPayment.paymentHash && selPayment.paymentHash.trim() !== '') {
this.dataService.decodePayment(selPayment.paymentHash, false)
.pipe(take(1))
.subscribe(sentPaymentInfo => {
this.showPaymentView(selPayment, sentPaymentInfo);
}, (error) => {
this.showPaymentView(selPayment, []);
});
} else {
this.showPaymentView(selPayment, []);
}
}
showPaymentView(selPayment: PaymentSent, sentPaymentInfo?: any[]) {
this.store.dispatch(new RTLActions.OpenAlert({ data: {
sentPaymentInfo: sentPaymentInfo,
payment: selPayment, payment: selPayment,
component: ECLPaymentInformationComponent component: ECLPaymentInformationComponent
}})); }}));
} }
onPartClick(selPart: PaymentSentPart, selPayment: PaymentSent) {
if (selPayment.paymentHash && selPayment.paymentHash.trim() !== '') {
this.dataService.decodePayment(selPayment.paymentHash, false)
.pipe(take(1))
.subscribe(sentPaymentInfo => {
this.showPartView(selPart, selPayment, sentPaymentInfo);
}, (error) => {
this.showPartView(selPart, selPayment, []);
});
} else {
this.showPartView(selPart, selPayment, []);
}
}
showPartView(selPart: PaymentSentPart, selPayment: PaymentSent, sentPaymentInfo?: any[]) {
const reorderedPart = [
[{key: 'paymentHash', value: selPayment.paymentHash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING}],
[{key: 'paymentPreimage', value: selPayment.paymentPreimage, title: 'Payment Preimage', width: 100, type: DataTypeEnum.STRING}],
[{key: 'toChannelId', value: selPart.toChannelId, title: 'Channel', width: 100, type: DataTypeEnum.STRING}],
[{key: 'id', value: selPart.id, title: 'Part ID', width: 50, type: DataTypeEnum.STRING},
{key: 'timestampStr', value: selPart.timestampStr, title: 'Time', width: 50, type: DataTypeEnum.DATE_TIME}],
[{key: 'amount', value: selPart.amount, title: 'Amount (Sats)', width: 50, type: DataTypeEnum.NUMBER},
{key: 'feesPaid', value: selPart.feesPaid, title: 'Fee (Sats)', width: 50, type: DataTypeEnum.NUMBER}]
];
if (sentPaymentInfo.length > 0 && sentPaymentInfo[0].paymentRequest && sentPaymentInfo[0].paymentRequest.description && sentPaymentInfo[0].paymentRequest.description !== '') {
reorderedPart.splice(3, 0, [{key: 'description', value: sentPaymentInfo[0].paymentRequest.description, title: 'Description', width: 100, type: DataTypeEnum.STRING}]);
}
this.store.dispatch(new RTLActions.OpenAlert({ data: {
type: AlertTypeEnum.INFORMATION,
alertTitle: 'Payment Part Information',
message: reorderedPart
}}));
}
applyFilter(selFilter: string) { applyFilter(selFilter: string) {
this.payments.filter = selFilter; this.payments.filter = selFilter;
} }
onDownloadCSV() { onDownloadCSV() {
if(this.payments.data && this.payments.data.length > 0) { if(this.payments.data && this.payments.data.length > 0) {
this.commonService.downloadFile(this.payments.data, 'Payments'); let paymentsDataCopy: PaymentSent[] = JSON.parse(JSON.stringify(this.payments.data));
let paymentRequests = paymentsDataCopy.reduce((paymentReqs, payment) => {
if (payment.paymentHash && payment.paymentHash.trim() !== '') {
paymentReqs = (paymentReqs === '') ? payment.paymentHash : paymentReqs + ',' + payment.paymentHash;
}
return paymentReqs;
}, '');
forkJoin(this.dataService.decodePayments(paymentRequests)
.pipe(takeUntil(this.unSubs[2]))
.subscribe((decodedPayments: any[][]) => {
decodedPayments.forEach((decodedPayment, idx) => {
if (decodedPayment.length > 0 && decodedPayment[0].paymentRequest && decodedPayment[0].paymentRequest.description && decodedPayment[0].paymentRequest.description !== '') {
paymentsDataCopy[idx].description = decodedPayment[0].paymentRequest.description;
}
});
let flattenedPayments = paymentsDataCopy.reduce((acc, curr) => acc.concat(curr), []);
this.commonService.downloadFile(flattenedPayments, 'Payments');
}));
} }
} }
ngOnDestroy() { ngOnDestroy() {

@ -13,6 +13,7 @@ import { PayRequest, Channel } from '../../../shared/models/eclModels';
import { CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, FEE_LIMIT_TYPES } from '../../../shared/services/consts-enums-functions'; import { CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, FEE_LIMIT_TYPES } from '../../../shared/services/consts-enums-functions';
import { CommonService } from '../../../shared/services/common.service'; import { CommonService } from '../../../shared/services/common.service';
import { LoggerService } from '../../../shared/services/logger.service'; import { LoggerService } from '../../../shared/services/logger.service';
import { DataService } from '../../../shared/services/data.service';
import { ECLEffects } from '../../store/ecl.effects'; import { ECLEffects } from '../../store/ecl.effects';
import * as ECLActions from '../../store/ecl.actions'; import * as ECLActions from '../../store/ecl.actions';
@ -41,7 +42,7 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
public paymentError = ''; public paymentError = '';
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<ECLLightningSendPaymentsComponent>, private store: Store<fromRTLReducer.RTLState>, private eclEffects: ECLEffects, private logger: LoggerService, private commonService: CommonService, private decimalPipe: DecimalPipe, private actions$: Actions) {} constructor(public dialogRef: MatDialogRef<ECLLightningSendPaymentsComponent>, private store: Store<fromRTLReducer.RTLState>, private eclEffects: ECLEffects, private logger: LoggerService, private commonService: CommonService, private decimalPipe: DecimalPipe, private actions$: Actions, private dataService: DataService) {}
ngOnInit() { ngOnInit() {
this.store.select('ecl') this.store.select('ecl')
@ -62,10 +63,6 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
delete this.paymentDecoded.amount; delete this.paymentDecoded.amount;
this.paymentError = action.payload.message; this.paymentError = action.payload.message;
} }
if (action.payload.action === 'DecodePayment') {
this.paymentDecodedHint = 'ERROR: ' + action.payload.message;
this.paymentReq.control.setErrors({'decodeError': true});
}
} }
}); });
} }
@ -79,9 +76,8 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
this.paymentError = ''; this.paymentError = '';
this.paymentDecodedHint = ''; this.paymentDecodedHint = '';
this.paymentReq.control.setErrors(null); this.paymentReq.control.setErrors(null);
this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...')); this.dataService.decodePayment(this.paymentRequest, true)
this.store.dispatch(new ECLActions.DecodePayment({routeParam: this.paymentRequest, fromDialog: true})); .pipe(take(1)).subscribe((decodedPayment: PayRequest) => {
this.eclEffects.setDecodedPayment.pipe(take(1)).subscribe(decodedPayment => {
this.paymentDecoded = decodedPayment; this.paymentDecoded = decodedPayment;
if (this.paymentDecoded.timestamp && !this.paymentDecoded.amount) { if (this.paymentDecoded.timestamp && !this.paymentDecoded.amount) {
this.paymentDecoded.amount = 0; this.paymentDecoded.amount = 0;
@ -99,6 +95,10 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
} }
}); });
} }
}, err => {
this.logger.error(err);
this.paymentDecodedHint = 'ERROR: ' + ((err.message) ? err.message : ((typeof err === 'string') ? err : JSON.stringify(err)));
this.paymentReq.control.setErrors({'decodeError': true});
}); });
} }
} }
@ -120,9 +120,8 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
if(this.paymentRequest && this.paymentRequest.length > 100) { if(this.paymentRequest && this.paymentRequest.length > 100) {
this.paymentReq.control.setErrors(null); this.paymentReq.control.setErrors(null);
this.zeroAmtInvoice = false; this.zeroAmtInvoice = false;
this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...')); this.dataService.decodePayment(this.paymentRequest, true)
this.store.dispatch(new ECLActions.DecodePayment({routeParam: this.paymentRequest, fromDialog: true})); .pipe(take(1)).subscribe((decodedPayment: PayRequest) => {
this.eclEffects.setDecodedPayment.subscribe(decodedPayment => {
this.paymentDecoded = decodedPayment; this.paymentDecoded = decodedPayment;
if (this.paymentDecoded.timestamp && !this.paymentDecoded.amount) { if (this.paymentDecoded.timestamp && !this.paymentDecoded.amount) {
this.paymentDecoded.amount = 0; this.paymentDecoded.amount = 0;
@ -140,6 +139,10 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
} }
}); });
} }
}, err => {
this.logger.error(err);
this.paymentDecodedHint = 'ERROR: ' + ((err.message) ? err.message : ((typeof err === 'string') ? err : JSON.stringify(err)));
this.paymentReq.control.setErrors({'decodeError': true});
}); });
} }
} }

@ -46,10 +46,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_channel"> <ng-container matColumnDef="no_channel">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!channels.data || channels.data.length<1">No channels available.</p> <p *ngIf="!channels?.data || channels?.data?.length<1">No channels available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_channel']" [ngClass]="{'display-none': channels.data && channels.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_channel']" [ngClass]="{'display-none': channels?.data && channels?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -5,11 +5,11 @@
<button mat-flat-button color="primary" tabindex="1" (click)="onRestoreChannels({})">Restore All</button> <button mat-flat-button color="primary" tabindex="1" (click)="onRestoreChannels({})">Restore All</button>
</div> </div>
</div> </div>
<div fxLayout="column" fxLayoutAlign="space-between start" fxLayout.gt-md="row wrap" *ngIf="!allRestoreExists && (!channels || channels.data.length<=0)"> <div fxLayout="column" fxLayoutAlign="space-between start" fxLayout.gt-md="row wrap" *ngIf="!allRestoreExists && (!channels || channels?.data?.length<=0)">
<h4 fxFlex="100">Restore folder location: {{selNode.channelBackupPath}}/restore</h4> <h4 fxFlex="100">Restore folder location: {{selNode.channelBackupPath}}/restore</h4>
<h4 fxFlex="100" class="mt-1">All channel backup file not found! To perform channel restoration, channel backup file/s must be placed at the above location.</h4> <h4 fxFlex="100" class="mt-1">All channel backup file not found! To perform channel restoration, channel backup file/s must be placed at the above location.</h4>
</div> </div>
<div fxLayout="column" fxLayoutAlign="space-between start" fxLayout.gt-md="row wrap" *ngIf="!allRestoreExists && channels && channels.data.length && channels.data.length>0"> <div fxLayout="column" fxLayoutAlign="space-between start" fxLayout.gt-md="row wrap" *ngIf="!allRestoreExists && channels && channels?.data?.length && channels?.data?.length>0">
<h4 fxFlex="100">Restore folder location: {{selNode.channelBackupPath}}/restore</h4> <h4 fxFlex="100">Restore folder location: {{selNode.channelBackupPath}}/restore</h4>
</div> </div>
<div fxLayout="column" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="padding-gap-x page-sub-title-container mt-2"> <div fxLayout="column" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="padding-gap-x page-sub-title-container mt-2">
@ -35,10 +35,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_channel"> <ng-container matColumnDef="no_channel">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!channels || !channels.data || channels.data.length<1">No singular channel backups available.</p> <p *ngIf="!channels || !channels.data || channels?.data?.length<1">No singular channel backups available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_channel']" [ngClass]="{'display-none': channels && channels.data && channels.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_channel']" [ngClass]="{'display-none': channels && channels.data && channels?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -22,10 +22,10 @@
<mat-card-content class="dashboard-card-content" fxLayout="column" fxFlex="95"> <mat-card-content class="dashboard-card-content" fxLayout="column" fxFlex="95">
<div [ngSwitch]="card.id" fxLayout="column" fxFlex="100"> <div [ngSwitch]="card.id" fxLayout="column" fxFlex="100">
<rtl-node-info fxFlex="100" *ngSwitchCase="'node'" [information]="information" [showColorFieldSeparately]="false" [ngClass]="{'error-border': flgLoading[0]==='error'}"></rtl-node-info> <rtl-node-info fxFlex="100" *ngSwitchCase="'node'" [information]="information" [showColorFieldSeparately]="false" [ngClass]="{'error-border': flgLoading[0]==='error'}"></rtl-node-info>
<rtl-balances-info fxFlex="100" *ngSwitchCase="'balance'" [balances]="balances" [ngClass]="{'error-border': flgLoading[2]==='error' || flgLoading[5]==='error'}"></rtl-balances-info> <rtl-balances-info fxFlex="100" *ngSwitchCase="'balance'" [balances]="balances" [ngClass]="{'error-border': flgLoading[2]==='error' || flgLoading[3]==='error'}"></rtl-balances-info>
<rtl-channel-capacity-info fxFlex="100" *ngSwitchCase="'capacity'" [sortBy]="sortField" [channelBalances]="channelBalances" [allChannels]="allChannelsCapacity" [ngClass]="{'error-border': flgLoading[5]==='error'}"></rtl-channel-capacity-info> <rtl-channel-capacity-info fxFlex="100" *ngSwitchCase="'capacity'" [sortBy]="sortField" [channelBalances]="channelBalances" [allChannels]="allChannelsCapacity" [ngClass]="{'error-border': flgLoading[3]==='error'}"></rtl-channel-capacity-info>
<rtl-fee-info fxFlex="100" *ngSwitchCase="'fee'" [fees]="fees" [ngClass]="{'error-border': flgLoading[1]==='error'}"></rtl-fee-info> <rtl-fee-info fxFlex="100" *ngSwitchCase="'fee'" [fees]="fees" [ngClass]="{'error-border': flgLoading[1]==='error'}"></rtl-fee-info>
<rtl-channel-status-info fxFlex="100" *ngSwitchCase="'status'" [channelsStatus]="channelsStatus" [ngClass]="{'error-border': flgLoading[5]==='error' || flgLoading[6]==='error'}"></rtl-channel-status-info> <rtl-channel-status-info fxFlex="100" *ngSwitchCase="'status'" [channelsStatus]="channelsStatus" [ngClass]="{'error-border': flgLoading[3]==='error' || flgLoading[4]==='error'}"></rtl-channel-status-info>
<h3 *ngSwitchDefault>Error! Unable to find information!</h3> <h3 *ngSwitchDefault>Error! Unable to find information!</h3>
</div> </div>
</mat-card-content> </mat-card-content>
@ -56,9 +56,9 @@
<mat-card-content class="dashboard-card-content" fxLayout="column" fxLayoutAlign="start stretch" fxFlex="{{card.id !== 'transactions' ? 95 : 100}}"> <mat-card-content class="dashboard-card-content" fxLayout="column" fxLayoutAlign="start stretch" fxFlex="{{card.id !== 'transactions' ? 95 : 100}}">
<div [ngSwitch]="card.id" fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch"> <div [ngSwitch]="card.id" fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch">
<rtl-node-info fxFlex="100" *ngSwitchCase="'node'" [information]="information" [ngClass]="{'error-border': flgLoading[0]==='error'}"></rtl-node-info> <rtl-node-info fxFlex="100" *ngSwitchCase="'node'" [information]="information" [ngClass]="{'error-border': flgLoading[0]==='error'}"></rtl-node-info>
<rtl-balances-info fxFlex="100" *ngSwitchCase="'balance'" [balances]="balances" [ngClass]="{'error-border': flgLoading[2]==='error' || flgLoading[5]==='error'}"></rtl-balances-info> <rtl-balances-info fxFlex="100" *ngSwitchCase="'balance'" [balances]="balances" [ngClass]="{'error-border': flgLoading[2]==='error' || flgLoading[3]==='error'}"></rtl-balances-info>
<rtl-channel-liquidity-info fxFlex="100" *ngSwitchCase="'inboundLiq'" [direction]="'In'" [totalLiquidity]="totalInboundLiquidity" [allChannels]="allInboundChannels" [ngClass]="{'error-border': flgLoading[5]==='error'}"></rtl-channel-liquidity-info> <rtl-channel-liquidity-info fxFlex="100" *ngSwitchCase="'inboundLiq'" [direction]="'In'" [totalLiquidity]="totalInboundLiquidity" [allChannels]="allInboundChannels" [ngClass]="{'error-border': flgLoading[3]==='error'}"></rtl-channel-liquidity-info>
<rtl-channel-liquidity-info fxFlex="100" *ngSwitchCase="'outboundLiq'" [direction]="'Out'" [totalLiquidity]="totalOutboundLiquidity" [allChannels]="allOutboundChannels" [ngClass]="{'error-border': flgLoading[5]==='error'}"></rtl-channel-liquidity-info> <rtl-channel-liquidity-info fxFlex="100" *ngSwitchCase="'outboundLiq'" [direction]="'Out'" [totalLiquidity]="totalOutboundLiquidity" [allChannels]="allOutboundChannels" [ngClass]="{'error-border': flgLoading[3]==='error'}"></rtl-channel-liquidity-info>
<span perfectScrollbar fxLayout="column" fxFlex="100" fxLayoutAlign="start start" *ngSwitchCase="'transactions'"> <span perfectScrollbar fxLayout="column" fxFlex="100" fxLayoutAlign="start start" *ngSwitchCase="'transactions'">
<mat-tab-group fxLayout="column" fxFlex="100" class="w-100 dashboard-tabs-group"> <mat-tab-group fxLayout="column" fxFlex="100" class="w-100 dashboard-tabs-group">
<mat-tab label="Receive"><rtl-lightning-invoices class="h-100" [showDetails]="false"></rtl-lightning-invoices></mat-tab> <mat-tab label="Receive"><rtl-lightning-invoices class="h-100" [showDetails]="false"></rtl-lightning-invoices></mat-tab>

@ -15,7 +15,6 @@ import { SelNodeChild } from '../../shared/models/RTLconfig';
import * as LNDActions from '../store/lnd.actions'; import * as LNDActions from '../store/lnd.actions';
import * as fromRTLReducer from '../../store/rtl.reducers'; import * as fromRTLReducer from '../../store/rtl.reducers';
import * as RTLActions from '../../store/rtl.actions';
@Component({ @Component({
selector: 'rtl-home', selector: 'rtl-home',
@ -111,52 +110,29 @@ export class HomeComponent implements OnInit, OnDestroy {
.subscribe((rtlStore) => { .subscribe((rtlStore) => {
this.flgLoading = [true, true, true, true, true, true, true, true]; this.flgLoading = [true, true, true, true, true, true, true, true];
rtlStore.effectErrors.forEach(effectsErr => { rtlStore.effectErrors.forEach(effectsErr => {
if (effectsErr.action === 'FetchInfo') { this.flgLoading[0] = (effectsErr.action === 'FetchInfo') ? 'error' : this.flgLoading[0];
this.flgLoading[0] = 'error'; this.flgLoading[1] = (effectsErr.action === 'FetchFees') ? 'error' : this.flgLoading[1];
} this.flgLoading[2] = (effectsErr.action === 'FetchBalance/channels') ? 'error' : this.flgLoading[2];
if (effectsErr.action === 'FetchFees') { this.flgLoading[3] = (effectsErr.action === 'FetchChannels/all') ? 'error' : this.flgLoading[3];
this.flgLoading[1] = 'error'; this.flgLoading[4] = (effectsErr.action === 'FetchChannels/pending') ? 'error' : this.flgLoading[4];
}
if (effectsErr.action === 'FetchBalance/blockchain') {
this.flgLoading[2] = 'error';
}
if (effectsErr.action === 'FetchBalance/channels') {
this.flgLoading[3] = 'error';
}
if (effectsErr.action === 'FetchChannels/all') {
this.flgLoading[5] = 'error';
}
if (effectsErr.action === 'FetchChannels/pending') {
this.flgLoading[6] = 'error';
}
}); });
this.flgLoading[0] = (rtlStore.information.identity_pubkey) ? false : this.flgLoading[0];
this.flgLoading[1] = (rtlStore.fees.day_fee_sum) ? false : this.flgLoading[1];
this.flgLoading[2] = (+rtlStore.blockchainBalance.total_balance >= 0 && rtlStore.totalLocalBalance >= 0) ? false : this.flgLoading[2];
this.flgLoading[3] = (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0) ? false : this.flgLoading[3];
this.flgLoading[4] = (this.flgLoading[4] !== 'error' && rtlStore.numberOfPendingChannels) ? false : this.flgLoading[4];
this.selNode = rtlStore.nodeSettings; this.selNode = rtlStore.nodeSettings;
this.showLoop = (this.selNode.swapServerUrl && this.selNode.swapServerUrl.trim() !== '') ? true : false; this.showLoop = (this.selNode.swapServerUrl && this.selNode.swapServerUrl.trim() !== '') ? true : false;
this.information = rtlStore.information; this.information = rtlStore.information;
if (this.flgLoading[0] !== 'error') {
this.flgLoading[0] = ( this.information.identity_pubkey) ? false : true;
}
this.fees = rtlStore.fees; this.fees = rtlStore.fees;
if (this.flgLoading[1] !== 'error') {
this.flgLoading[1] = ( this.fees.day_fee_sum) ? false : true;
}
this.balances.onchain = (+rtlStore.blockchainBalance.total_balance >= 0) ? +rtlStore.blockchainBalance.total_balance : 0; this.balances.onchain = (+rtlStore.blockchainBalance.total_balance >= 0) ? +rtlStore.blockchainBalance.total_balance : 0;
if (this.flgLoading[2] !== 'error') {
this.flgLoading[2] = false;
}
let local = (rtlStore.totalLocalBalance) ? +rtlStore.totalLocalBalance : 0; let local = (rtlStore.totalLocalBalance) ? +rtlStore.totalLocalBalance : 0;
let remote = (rtlStore.totalRemoteBalance) ? +rtlStore.totalRemoteBalance : 0; let remote = (rtlStore.totalRemoteBalance) ? +rtlStore.totalRemoteBalance : 0;
let total = local + remote; let total = local + remote;
this.channelBalances = { localBalance: local, remoteBalance: remote, balancedness: (1 - Math.abs((local-remote)/total)).toFixed(3) }; this.channelBalances = { localBalance: local, remoteBalance: remote, balancedness: (1 - Math.abs((local-remote)/total)).toFixed(3) };
this.balances.lightning = rtlStore.totalLocalBalance; this.balances.lightning = rtlStore.totalLocalBalance;
if (this.flgLoading[5] !== 'error') {
this.flgLoading[5] = false;
}
this.balances.total = this.balances.lightning + this.balances.onchain; this.balances.total = this.balances.lightning + this.balances.onchain;
this.balances = Object.assign({}, this.balances); this.balances = Object.assign({}, this.balances);
this.activeChannels = rtlStore.numberOfActiveChannels; this.activeChannels = rtlStore.numberOfActiveChannels;
this.inactiveChannels = rtlStore.numberOfInactiveChannels; this.inactiveChannels = rtlStore.numberOfInactiveChannels;
this.channelsStatus = { this.channelsStatus = {
@ -168,12 +144,6 @@ export class HomeComponent implements OnInit, OnDestroy {
capacity: rtlStore.numberOfPendingChannels.total_limbo_balance capacity: rtlStore.numberOfPendingChannels.total_limbo_balance
} }
}; };
if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0 && this.flgLoading[5] !== 'error') {
this.flgLoading[5] = false;
}
if (rtlStore.numberOfPendingChannels && this.flgLoading[6] !== 'error') {
this.flgLoading[6] = false;
}
this.totalInboundLiquidity = 0; this.totalInboundLiquidity = 0;
this.totalOutboundLiquidity = 0; this.totalOutboundLiquidity = 0;
this.allChannels = rtlStore.allChannels.filter(channel => channel.active === true); this.allChannels = rtlStore.allChannels.filter(channel => channel.active === true);
@ -184,7 +154,6 @@ export class HomeComponent implements OnInit, OnDestroy {
this.totalInboundLiquidity = this.totalInboundLiquidity + +channel.remote_balance; this.totalInboundLiquidity = this.totalInboundLiquidity + +channel.remote_balance;
this.totalOutboundLiquidity = this.totalOutboundLiquidity + +channel.local_balance; this.totalOutboundLiquidity = this.totalOutboundLiquidity + +channel.local_balance;
}); });
if (this.balances.lightning >= 0 && this.balances.onchain >= 0 && this.fees.month_fee_sum >= 0) { if (this.balances.lightning >= 0 && this.balances.onchain >= 0 && this.fees.month_fee_sum >= 0) {
this.flgChildInfoUpdated = true; this.flgChildInfoUpdated = true;
} else { } else {

@ -12,7 +12,9 @@ import { PeersComponent } from './peers-channels/peers/peers.component';
import { LightningInvoicesComponent } from './transactions/invoices/lightning-invoices.component'; import { LightningInvoicesComponent } from './transactions/invoices/lightning-invoices.component';
import { OnChainReceiveComponent } from './on-chain/on-chain-receive/on-chain-receive.component'; import { OnChainReceiveComponent } from './on-chain/on-chain-receive/on-chain-receive.component';
import { OnChainComponent } from './on-chain/on-chain.component'; import { OnChainComponent } from './on-chain/on-chain.component';
import { OnChainTransactionHistoryComponent } from './on-chain/on-chain-transaction-history/on-chain-transaction-history.component'; import { UTXOTablesComponent } from './on-chain/utxo-tables/utxo-tables.component';
import { OnChainUTXOsComponent } from './on-chain/utxo-tables/utxos/utxos.component';
import { OnChainTransactionHistoryComponent } from './on-chain/utxo-tables/on-chain-transaction-history/on-chain-transaction-history.component';
import { WalletComponent } from './wallet/wallet.component'; import { WalletComponent } from './wallet/wallet.component';
import { LightningPaymentsComponent } from './transactions/payments/lightning-payments.component'; import { LightningPaymentsComponent } from './transactions/payments/lightning-payments.component';
import { ChannelPendingTableComponent } from './peers-channels/channels/channels-tables/channel-pending-table/channel-pending-table.component'; import { ChannelPendingTableComponent } from './peers-channels/channels/channels-tables/channel-pending-table/channel-pending-table.component';
@ -32,6 +34,7 @@ import { SignComponent } from './sign-verify-message/sign/sign.component';
import { VerifyComponent } from './sign-verify-message/verify/verify.component'; import { VerifyComponent } from './sign-verify-message/verify/verify.component';
import { QueryRoutesComponent } from './transactions/query-routes/query-routes.component'; import { QueryRoutesComponent } from './transactions/query-routes/query-routes.component';
import { ChannelOpenTableComponent } from './peers-channels/channels/channels-tables/channel-open-table/channel-open-table.component'; import { ChannelOpenTableComponent } from './peers-channels/channels/channels-tables/channel-open-table/channel-open-table.component';
import { ChannelActiveHTLCsTableComponent } from './peers-channels/channels/channels-tables/channel-active-htlcs-table/channel-active-htlcs-table.component';
import { UnlockWalletComponent } from './wallet/unlock/unlock.component'; import { UnlockWalletComponent } from './wallet/unlock/unlock.component';
import { InitializeWalletComponent } from './wallet/initialize/initialize.component'; import { InitializeWalletComponent } from './wallet/initialize/initialize.component';
import { NodeInfoComponent } from './home/node-info/node-info.component'; import { NodeInfoComponent } from './home/node-info/node-info.component';
@ -78,9 +81,12 @@ import { LNDUnlockedGuard } from '../shared/services/auth.guard';
QueryRoutesComponent, QueryRoutesComponent,
OnChainReceiveComponent, OnChainReceiveComponent,
OnChainComponent, OnChainComponent,
UTXOTablesComponent,
OnChainUTXOsComponent,
OnChainTransactionHistoryComponent, OnChainTransactionHistoryComponent,
ChannelsTablesComponent, ChannelsTablesComponent,
ChannelOpenTableComponent, ChannelOpenTableComponent,
ChannelActiveHTLCsTableComponent,
UnlockWalletComponent, UnlockWalletComponent,
InitializeWalletComponent, InitializeWalletComponent,
NodeInfoComponent, NodeInfoComponent,

@ -94,7 +94,7 @@
<h4 *ngIf="loopStatus" fxLayoutAlign="start" class="font-bold-500 mt-2">{{(loopStatus && loopStatus.error) ? (loopDirectionCaption + ' failed.') : (loopStatus && loopStatus.id_bytes && channel) ? (loopDirectionCaption + ' request placed successfully. Go to loop to check it\'s status.') : (loopDirectionCaption + ' request placed successfully.')}}</h4> <h4 *ngIf="loopStatus" fxLayoutAlign="start" class="font-bold-500 mt-2">{{(loopStatus && loopStatus.error) ? (loopDirectionCaption + ' failed.') : (loopStatus && loopStatus.id_bytes && channel) ? (loopDirectionCaption + ' request placed successfully. Go to loop to check it\'s status.') : (loopDirectionCaption + ' request placed successfully.')}}</h4>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100"> <div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button *ngIf="loopStatus && loopStatus.id_bytes && channel" mat-flat-button color="primary" tabindex="12" type="button" (click)="goToLoop()">Check Status</button> <button *ngIf="loopStatus && loopStatus.id_bytes && channel" mat-flat-button color="primary" tabindex="12" type="button" (click)="goToLoop()">Check Status</button>
<button *ngIf="loopStatus && (loopStatus.error || !loopStatus.id_bytes)" mat-flat-button color="primary" tabindex="13" type="button" (click)="stepper.reset()">Start Again</button> <button *ngIf="loopStatus && (loopStatus.error || !loopStatus.id_bytes)" mat-flat-button color="primary" tabindex="13" type="button" (click)="onRestart()">Start Again</button>
</div> </div>
</form> </form>
</mat-step> </mat-step>

@ -114,14 +114,14 @@ export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
} }
onLoop() { onLoop() {
if(!this.inputFormGroup.controls.amount.value || this.inputFormGroup.controls.amount.value < this.minQuote.amount || this.inputFormGroup.controls.amount.value > this.maxQuote.amount || !this.inputFormGroup.controls.sweepConfTarget.value || this.inputFormGroup.controls.sweepConfTarget.value < 2 || (!this.inputFormGroup.controls.routingFeePercent.value || this.inputFormGroup.controls.routingFeePercent.value < 0 || this.inputFormGroup.controls.routingFeePercent.value > this.maxRoutingFeePercentage) || (this.direction === SwapTypeEnum.LOOP_OUT && this.addressFormGroup.controls.addressType.value === 'external' && (!this.addressFormGroup.controls.address.value || this.addressFormGroup.controls.address.value.trim() === ''))) { return true; } if(!this.inputFormGroup.controls.amount.value || this.inputFormGroup.controls.amount.value < this.minQuote.amount || this.inputFormGroup.controls.amount.value > this.maxQuote.amount || !this.inputFormGroup.controls.sweepConfTarget.value || this.inputFormGroup.controls.sweepConfTarget.value < 2 || (this.direction === SwapTypeEnum.LOOP_OUT && (!this.inputFormGroup.controls.routingFeePercent.value || this.inputFormGroup.controls.routingFeePercent.value < 0 || this.inputFormGroup.controls.routingFeePercent.value > this.maxRoutingFeePercentage)) || (this.direction === SwapTypeEnum.LOOP_OUT && this.addressFormGroup.controls.addressType.value === 'external' && (!this.addressFormGroup.controls.address.value || this.addressFormGroup.controls.address.value.trim() === ''))) { return true; }
this.flgEditable = false; this.flgEditable = false;
this.stepper.selected.stepControl.setErrors(null); this.stepper.selected.stepControl.setErrors(null);
this.stepper.next(); this.stepper.next();
if (this.direction === SwapTypeEnum.LOOP_IN) { if (this.direction === SwapTypeEnum.LOOP_IN) {
this.loopService.loopIn(this.inputFormGroup.controls.amount.value, +this.quote.swap_fee_sat, +this.quote.htlc_publish_fee_sat, '', true).pipe(takeUntil(this.unSubs[0])) this.loopService.loopIn(this.inputFormGroup.controls.amount.value, +this.quote.swap_fee_sat, +this.quote.htlc_publish_fee_sat, '', true).pipe(takeUntil(this.unSubs[0]))
.subscribe((loopStatus: any) => { .subscribe((loopStatus: any) => {
this.loopStatus = JSON.parse(loopStatus); this.loopStatus = loopStatus;
this.store.dispatch(new LNDActions.FetchLoopSwaps()); this.store.dispatch(new LNDActions.FetchLoopSwaps());
this.flgEditable = true; this.flgEditable = true;
}, (err) => { }, (err) => {
@ -135,7 +135,7 @@ export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
let swapPublicationDeadline = this.inputFormGroup.controls.fast.value ? 0 : new Date().getTime() + (30 * 60000); let swapPublicationDeadline = this.inputFormGroup.controls.fast.value ? 0 : new Date().getTime() + (30 * 60000);
this.loopService.loopOut(this.inputFormGroup.controls.amount.value, (this.channel && this.channel.chan_id ? this.channel.chan_id : ''), this.inputFormGroup.controls.sweepConfTarget.value, swapRoutingFee, +this.quote.htlc_sweep_fee_sat, this.prepayRoutingFee, +this.quote.prepay_amt_sat, +this.quote.swap_fee_sat, swapPublicationDeadline, destAddress).pipe(takeUntil(this.unSubs[1])) this.loopService.loopOut(this.inputFormGroup.controls.amount.value, (this.channel && this.channel.chan_id ? this.channel.chan_id : ''), this.inputFormGroup.controls.sweepConfTarget.value, swapRoutingFee, +this.quote.htlc_sweep_fee_sat, this.prepayRoutingFee, +this.quote.prepay_amt_sat, +this.quote.swap_fee_sat, swapPublicationDeadline, destAddress).pipe(takeUntil(this.unSubs[1]))
.subscribe((loopStatus: any) => { .subscribe((loopStatus: any) => {
this.loopStatus = JSON.parse(loopStatus); this.loopStatus = loopStatus;
this.store.dispatch(new LNDActions.FetchLoopSwaps()); this.store.dispatch(new LNDActions.FetchLoopSwaps());
this.flgEditable = true; this.flgEditable = true;
}, (err) => { }, (err) => {
@ -148,8 +148,6 @@ export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
onEstimateQuote() { onEstimateQuote() {
if(!this.inputFormGroup.controls.amount.value || this.inputFormGroup.controls.amount.value < this.minQuote.amount || this.inputFormGroup.controls.amount.value > this.maxQuote.amount || !this.inputFormGroup.controls.sweepConfTarget.value || this.inputFormGroup.controls.sweepConfTarget.value < 2) { return true; } if(!this.inputFormGroup.controls.amount.value || this.inputFormGroup.controls.amount.value < this.minQuote.amount || this.inputFormGroup.controls.amount.value > this.maxQuote.amount || !this.inputFormGroup.controls.sweepConfTarget.value || this.inputFormGroup.controls.sweepConfTarget.value < 2) { return true; }
this.stepper.selected.stepControl.setErrors(null);
this.stepper.next();
this.store.dispatch(new RTLActions.OpenSpinner('Getting Quotes...')); this.store.dispatch(new RTLActions.OpenSpinner('Getting Quotes...'));
let swapPublicationDeadline = this.inputFormGroup.controls.fast.value ? 0 : new Date().getTime() + (30 * 60000); let swapPublicationDeadline = this.inputFormGroup.controls.fast.value ? 0 : new Date().getTime() + (30 * 60000);
if(this.direction === SwapTypeEnum.LOOP_IN) { if(this.direction === SwapTypeEnum.LOOP_IN) {
@ -169,6 +167,8 @@ export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
this.quote.off_chain_swap_routing_fee_percentage = this.inputFormGroup.controls.routingFeePercent.value ? this.inputFormGroup.controls.routingFeePercent.value : 2; this.quote.off_chain_swap_routing_fee_percentage = this.inputFormGroup.controls.routingFeePercent.value ? this.inputFormGroup.controls.routingFeePercent.value : 2;
}); });
} }
this.stepper.selected.stepControl.setErrors(null);
this.stepper.next();
} }
stepSelectionChanged(event: any) { stepSelectionChanged(event: any) {
@ -252,7 +252,17 @@ export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
onStepChanged(index: number) { onStepChanged(index: number) {
this.animationDirection = index < this.stepNumber ? 'backward' : 'forward'; this.animationDirection = index < this.stepNumber ? 'backward' : 'forward';
this.stepNumber = index; this.stepNumber = index;
} }
onRestart() {
this.stepper.reset();
this.flgEditable = true;
this.inputFormGroup.reset({ amount: this.minQuote.amount, sweepConfTarget: 6, routingFeePercent: this.maxRoutingFeePercentage, fast: false });
this.quoteFormGroup.reset();
this.statusFormGroup.reset();
this.addressFormGroup.reset({addressType: 'local', address: ''});
this.addressFormGroup.controls.address.disable();
}
ngOnDestroy() { ngOnDestroy() {
this.unSubs.forEach(completeSub => { this.unSubs.forEach(completeSub => {

@ -71,10 +71,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_swap"> <ng-container matColumnDef="no_swap">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!listSwaps.data || listSwaps.data.length<1">No {{swapCaption | lowercase}} swaps available.</p> <p *ngIf="!listSwaps?.data || listSwaps?.data?.length<1">{{emptyTableMessage}}</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_swap']" [ngClass]="{'display-none': listSwaps.data && listSwaps.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_swap']" [ngClass]="{'display-none': listSwaps?.data && listSwaps?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -38,6 +38,7 @@ export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
public storedSwaps: SwapStatus[] = []; public storedSwaps: SwapStatus[] = [];
public filteredSwaps: SwapStatus[] = []; public filteredSwaps: SwapStatus[] = [];
public flgLoading: Array<Boolean | 'error'> = [true]; public flgLoading: Array<Boolean | 'error'> = [true];
public emptyTableMessage = 'No swaps available.';
public flgSticky = false; public flgSticky = false;
public pageSize = PAGE_SIZE; public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS; public pageSizeOptions = PAGE_SIZE_OPTIONS;
@ -68,7 +69,10 @@ export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
.pipe(takeUntil(this.unSubs[1])) .pipe(takeUntil(this.unSubs[1]))
.subscribe((rtlStore) => { .subscribe((rtlStore) => {
rtlStore.effectErrors.forEach(effectsErr => { rtlStore.effectErrors.forEach(effectsErr => {
if (effectsErr.action === 'FetchSwaps') { this.flgLoading[0] = 'error'; } if (effectsErr.action === 'FetchSwaps') {
this.flgLoading[0] = 'error';
this.emptyTableMessage = 'ERROR: ' + effectsErr.message;
}
}); });
if (rtlStore.loopSwaps) { if (rtlStore.loopSwaps) {
this.storedSwaps = rtlStore.loopSwaps; this.storedSwaps = rtlStore.loopSwaps;
@ -76,7 +80,7 @@ export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
this.loadSwapsTable(this.filteredSwaps); this.loadSwapsTable(this.filteredSwaps);
} }
if (this.flgLoading[0] !== 'error') { if (this.flgLoading[0] !== 'error') {
this.flgLoading[0] = ( rtlStore.transactions) ? false : true; this.flgLoading[0] = (rtlStore.loopSwaps) ? false : true;
} }
this.logger.info(rtlStore); this.logger.info(rtlStore);
}); });
@ -84,7 +88,8 @@ export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
} }
ngOnChanges() { ngOnChanges() {
this.swapCaption = (this.selectedSwapType === SwapTypeEnum.LOOP_IN) ? 'Loop In' : 'Loop Out' this.swapCaption = (this.selectedSwapType === SwapTypeEnum.LOOP_IN) ? 'Loop In' : 'Loop Out';
this.emptyTableMessage = 'No ' + this.swapCaption.toLowerCase() + ' swaps available.';
this.filteredSwaps = this.storedSwaps.filter(swap => swap.type === this.selectedSwapType); this.filteredSwaps = this.storedSwaps.filter(swap => swap.type === this.selectedSwapType);
this.loadSwapsTable(this.filteredSwaps); this.loadSwapsTable(this.filteredSwaps);
} }

@ -85,19 +85,15 @@ export class OnChainSendComponent implements OnInit, OnDestroy {
this.confirmFormGroup = this.formBuilder.group({}); this.confirmFormGroup = this.formBuilder.group({});
this.sendFundFormGroup.controls.selTransType.valueChanges.pipe(takeUntil(this.unSubs[0])).subscribe(transType => { this.sendFundFormGroup.controls.selTransType.valueChanges.pipe(takeUntil(this.unSubs[0])).subscribe(transType => {
if (transType === '1') { if (transType === '1') {
this.sendFundFormGroup.controls.transactionBlocks.setValue(null);
this.sendFundFormGroup.controls.transactionBlocks.setValidators([Validators.required]); this.sendFundFormGroup.controls.transactionBlocks.setValidators([Validators.required]);
this.sendFundFormGroup.controls.transactionBlocks.setErrors(null); this.sendFundFormGroup.controls.transactionBlocks.setValue(null);
this.sendFundFormGroup.controls.transactionFees.setValue(null);
this.sendFundFormGroup.controls.transactionFees.setValidators(null); this.sendFundFormGroup.controls.transactionFees.setValidators(null);
this.sendFundFormGroup.controls.transactionFees.setErrors(null); this.sendFundFormGroup.controls.transactionFees.setValue(null);
} else { } else {
this.sendFundFormGroup.controls.transactionBlocks.setValue(null);
this.sendFundFormGroup.controls.transactionBlocks.setValidators(null); this.sendFundFormGroup.controls.transactionBlocks.setValidators(null);
this.sendFundFormGroup.controls.transactionBlocks.setErrors(null); this.sendFundFormGroup.controls.transactionBlocks.setValue(null);
this.sendFundFormGroup.controls.transactionFees.setValue(null);
this.sendFundFormGroup.controls.transactionFees.setValidators([Validators.required]); this.sendFundFormGroup.controls.transactionFees.setValidators([Validators.required]);
this.sendFundFormGroup.controls.transactionFees.setErrors(null); this.sendFundFormGroup.controls.transactionFees.setValue(null);
} }
}); });
this.store.select('root') this.store.select('root')

@ -31,14 +31,12 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap"> <div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap">
<div fxLayout="row"> <div fxLayout="row">
<button mat-flat-button color="primary" type="button" tabindex="3" (click)="openSendFundsModal(true)">Sweep All</button> <button mat-flat-button color="primary" type="button" tabindex="3" (click)="openSendFundsModal(true)">Sweep All</button>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large"> <div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large">
<div fxLayout="row"> <rtl-utxo-tables fxLayout="row" fxFlex="100"></rtl-utxo-tables>
<rtl-on-chain-transaction-history fxLayout="row" fxFlex="100"></rtl-on-chain-transaction-history>
</div>
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

@ -2,7 +2,6 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
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';
import { Actions } from '@ngrx/effects';
import { faExchangeAlt, faChartPie } from '@fortawesome/free-solid-svg-icons'; import { faExchangeAlt, faChartPie } from '@fortawesome/free-solid-svg-icons';
import { SelNodeChild } from '../../shared/models/RTLconfig'; import { SelNodeChild } from '../../shared/models/RTLconfig';
@ -22,7 +21,7 @@ export class OnChainComponent implements OnInit, OnDestroy {
public balances = [{title: 'Total Balance', dataValue: 0}, {title: 'Confirmed', dataValue: 0}, {title: 'Unconfirmed', dataValue: 0}]; public balances = [{title: 'Total Balance', dataValue: 0}, {title: 'Confirmed', dataValue: 0}, {title: 'Unconfirmed', dataValue: 0}];
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private store: Store<fromRTLReducer.RTLState>, private actions$: Actions) {} constructor(private store: Store<fromRTLReducer.RTLState>) {}
ngOnInit() { ngOnInit() {
this.store.select('lnd') this.store.select('lnd')

@ -1,18 +1,15 @@
<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="row wrap" fxLayoutAlign="start start" fxLayout.gt-sm="column" fxFlex="100" fxLayoutAlign.gt-sm="start stretch" class="padding-gap-x-large">
<div fxLayout="column" fxLayout.gt-xs="row wrap" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container"> <div fxLayout="column" fxLayout.gt-xs="row wrap" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70"> <div fxFlex="70"></div>
<fa-icon [icon]="faHistory" class="page-title-img mr-1"></fa-icon>
<span class="page-title">Transaction History</span>
</div>
<mat-form-field fxFlex="30"> <mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter"> <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field> </mat-form-field>
</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="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="errorLoading===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="listTransactions" matSort <table mat-table #table [dataSource]="listTransactions" matSort
[ngClass]="{'overflow-auto error-border': flgLoading[0]==='error','overflow-auto': true}"> [ngClass]="{'overflow-auto error-border': errorLoading==='error','overflow-auto': true}">
<ng-container matColumnDef="time_stamp"> <ng-container matColumnDef="time_stamp">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Date/Time </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Date/Time </th>
<td mat-cell *matCellDef="let transaction">{{transaction.time_stamp_str}}</td> <td mat-cell *matCellDef="let transaction">{{transaction.time_stamp_str}}</td>
@ -52,10 +49,10 @@
</ng-container> </ng-container>
<ng-container matColumnDef="no_transaction"> <ng-container matColumnDef="no_transaction">
<td mat-footer-cell *matFooterCellDef colspan="4"> <td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!listTransactions.data || listTransactions.data.length<1">No transactions available.</p> <p *ngIf="!listTransactions?.data || listTransactions?.data?.length<1">No transactions available.</p>
</td> </td>
</ng-container> </ng-container>
<tr mat-footer-row *matFooterRowDef="['no_transaction']" [ngClass]="{'display-none': listTransactions.data && listTransactions.data.length>0}"></tr> <tr mat-footer-row *matFooterRowDef="['no_transaction']" [ngClass]="{'display-none': listTransactions?.data && listTransactions?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

@ -1,6 +1,4 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { Component, ViewChild, Input, OnChanges } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects'; import { Actions } from '@ngrx/effects';
import { faHistory } from '@fortawesome/free-solid-svg-icons'; import { faHistory } from '@fortawesome/free-solid-svg-icons';
@ -8,14 +6,13 @@ import { faHistory } from '@fortawesome/free-solid-svg-icons';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator'; 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 { Transaction } from '../../../shared/models/lndModels'; import { Transaction } from '../../../../shared/models/lndModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum } from '../../../shared/services/consts-enums-functions'; import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum } from '../../../../shared/services/consts-enums-functions';
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 * as LNDActions from '../../store/lnd.actions'; import * as RTLActions from '../../../../store/rtl.actions';
import * as RTLActions from '../../../store/rtl.actions'; import * as fromRTLReducer from '../../../../store/rtl.reducers';
import * as fromRTLReducer from '../../../store/rtl.reducers';
@Component({ @Component({
selector: 'rtl-on-chain-transaction-history', selector: 'rtl-on-chain-transaction-history',
@ -25,19 +22,19 @@ import * as fromRTLReducer from '../../../store/rtl.reducers';
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Transactions') } { provide: MatPaginatorIntl, useValue: getPaginatorLabel('Transactions') }
] ]
}) })
export class OnChainTransactionHistoryComponent implements OnInit, OnDestroy { export class OnChainTransactionHistoryComponent implements OnChanges {
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator; @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
@Input() transactions: Transaction[];
@Input() errorLoading: any;
faHistory = faHistory; faHistory = faHistory;
public displayedColumns = []; public displayedColumns = [];
public listTransactions: any; public listTransactions: any;
public flgLoading: Array<Boolean | 'error'> = [true];
public flgSticky = false; 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 = '';
public screenSizeEnum = ScreenSizeEnum; public screenSizeEnum = ScreenSizeEnum;
private unsub: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions) { constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions) {
this.screenSize = this.commonService.getScreenSize(); this.screenSize = this.commonService.getScreenSize();
@ -56,25 +53,10 @@ export class OnChainTransactionHistoryComponent implements OnInit, OnDestroy {
} }
} }
ngOnInit() { ngOnChanges() {
this.store.dispatch(new LNDActions.FetchTransactions()); if (this.transactions && this.transactions.length > 0) {
this.store.select('lnd') this.loadTransactionsTable(this.transactions);
.pipe(takeUntil(this.unsub[0])) }
.subscribe((rtlStore) => {
rtlStore.effectErrors.forEach(effectsErr => {
if (effectsErr.action === 'FetchTransactions') {
this.flgLoading[0] = 'error';
}
});
if ( rtlStore.transactions) {
this.loadTransactionsTable(rtlStore.transactions);
}
if (this.flgLoading[0] !== 'error') {
this.flgLoading[0] = ( rtlStore.transactions) ? false : true;
}
this.logger.info(rtlStore);
});
} }
applyFilter(selFilter: string) { applyFilter(selFilter: string) {
@ -114,11 +96,4 @@ export class OnChainTransactionHistoryComponent implements OnInit, OnDestroy {
} }
} }
ngOnDestroy() {
this.unsub.forEach(completeSub => {
completeSub.next();
completeSub.complete();
});
}
} }

@ -0,0 +1,16 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch" class="bordered-box">
<mat-tab-group>
<mat-tab>
<ng-template mat-tab-label>
<span matBadge="{{numUtxos}}" matBadgeOverlap="false" class="tab-badge">UTXOs</span>
</ng-template>
<rtl-on-chain-utxos [utxos]="utxos" [errorLoading]="flgLoading[0]" fxLayout="row" fxFlex="100"></rtl-on-chain-utxos>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<span matBadge="{{numTransactions}}" matBadgeOverlap="false" class="tab-badge">Transactions</span>
</ng-template>
<rtl-on-chain-transaction-history [transactions]="transactions" [errorLoading]="flgLoading[1]" fxLayout="row" fxFlex="100"></rtl-on-chain-transaction-history>
</mat-tab>
</mat-tab-group>
</div>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UTXOTablesComponent } from './utxo-tables.component';
describe('UTXOTablesComponent', () => {
let component: UTXOTablesComponent;
let fixture: ComponentFixture<UTXOTablesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UTXOTablesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UTXOTablesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

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

Loading…
Cancel
Save