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>
[![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)
* [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">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="styles.e251e2de33649e5868e8.css"></head>
<link rel="stylesheet" href="styles.a87bd00d80a3f00717e3.css"></head>
<body>
<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>

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

@ -10,6 +10,7 @@ var connect = {};
var errMsg = '';
var request = require('request');
var ini = require('ini');
var parseHocon = require('hocon-parser');
common.path_separator = (platform === 'win32') ? '\\' : '/';
connect.setDefaultConfig = () => {
@ -150,7 +151,9 @@ connect.validateNodeConfig = (config) => {
let exists = fs.existsSync(common.nodes[idx].config_path);
if (exists) {
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) {
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});
fs.readFile(file, 'utf8', function(err, data) {
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({
message: "Reading File Failed!",
error: err

@ -100,8 +100,9 @@ exports.setChannelFee = (req, res, next) => {
}
exports.closeChannel = (req, res, next) => {
req.setTimeout(60000 * 10); // timeout 10 mins
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;
logger.info({fileName: 'Channels', msg: 'Closing Channel: ' + options.url});
request.delete(options).then((body) => {

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

@ -13,21 +13,60 @@ exports.getFees = (req, res, next) => {
to: tillToday
};
request.post(options).then((body) => {
logger.info({fileName: 'Fees', msg: 'Audit Response: ' + JSON.stringify(body)});
let resBody = {
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 : []
}
};
logger.info({fileName: 'Fees', msg: 'Fee Response: ' + JSON.stringify(body)});
let fees = { daily_fee: 0, daily_txs: 0, weekly_fee: 0, weekly_txs: 0, monthly_fee: 0, monthly_txs: 0 };
let current_time = Math.round((new Date().getTime()));
let month_start_time = current_time - 2629743000;
let week_start_time = current_time - 604800000;
let day_start_time = current_time - 86400000;
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.parts && sentEle.parts.length > 0) {
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); }
});
});
resBody.payments.received.forEach(receivedEle => {
payments.received.forEach(receivedEle => {
if (receivedEle.parts && receivedEle.parts.length > 0) {
receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp;
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); }
});
});
resBody.payments.relayed.forEach(relayedEle => {
logger.info({fileName: 'Fees', msg: 'Relayed Transaction: ' + JSON.stringify(relayedEle)});
payments.relayed.forEach(relayedEle => {
logger.info({fileName: 'Fees', msg: 'Payment Relayed Transaction: ' + JSON.stringify(relayedEle)});
relayedEle.timestampStr = (!relayedEle.timestamp) ? '' : common.convertTimestampToDate(Math.round(relayedEle.timestamp / 1000));
if (relayedEle.amountIn) { relayedEle.amountIn = Math.round(relayedEle.amountIn/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');
resBody.payments.received = common.sortDescByKey(resBody.payments.received, 'firstPartTimestamp');
resBody.payments.relayed = common.sortDescByKey(resBody.payments.relayed, 'timestamp');
logger.info({fileName: 'Fees', msg: JSON.stringify(resBody)});
res.status(200).json(resBody);
payments.sent = common.sortDescByKey(payments.sent, 'firstPartTimestamp');
payments.received = common.sortDescByKey(payments.received, 'firstPartTimestamp');
payments.relayed = common.sortDescByKey(payments.relayed, 'timestamp');
logger.info({fileName: 'Fees', msg: JSON.stringify(payments)});
res.status(200).json(payments);
})
.catch(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) {
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({
message: "Fetching Fees failed!",
message: "Fetching Payments failed!",
error: err.error && err.error.error ? err.error.error : err.error ? err.error : "Unknown Server Error"
});
});

@ -5,6 +5,7 @@ var options = {};
var pendingInvoices = [];
getReceivedPaymentInfo = (invoice) => {
logger.info({fileName: 'Invoice', msg: 'Invoice Received: ' + JSON.stringify(invoice)});
let idx = -1;
return new Promise(function(resolve, reject) {
invoice.timestampStr = (!invoice.timestamp) ? '' : common.convertTimestampToDate(invoice.timestamp);
@ -17,10 +18,10 @@ getReceivedPaymentInfo = (invoice) => {
options.form = { paymentHash: invoice.paymentHash };
request(options).then(response => {
invoice.status = response.status.type;
invoice.amount = Math.round(response.status.amount/1000);
if (response.status.receivedAt) {
invoice.receivedAt = Math.round(response.status.receivedAt / 1000);
invoice.receivedAtStr = common.convertTimestampToDate(invoice.receivedAt);
if (response.status && response.status.type === 'received') {
invoice.amountSettled = response.status.amount ? Math.round(response.status.amount/1000) : 0;
invoice.receivedAt = response.status.receivedAt ? Math.round(response.status.receivedAt / 1000) : 0;
invoice.receivedAtStr = response.status.receivedAt ? common.convertTimestampToDate(invoice.receivedAt) : '';
}
resolve(invoice);
}).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') {
return res.status(500).json({
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';
@ -87,7 +92,7 @@ exports.connectPeer = (req, res, next) => {
peer.alias = foundPeer ? foundPeer.alias : peer.nodeId.substring(0, 20);
});
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 Added Successfully'});
res.status(201).json(peers);

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

@ -2,13 +2,12 @@ var request = require('request-promise');
var common = require('../../common');
var logger = require('../logger');
var options = {};
var swapServerUrl = '';
exports.loopOut = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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';
let body = {
options = common.getSwapServerOptions();
if(options.url === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = options.url + '/v1/loop/out';
options.body = {
amt: req.body.amount,
sweep_conf_target: req.body.targetConf,
max_swap_routing_fee: req.body.swapRoutingFee,
@ -18,20 +17,19 @@ exports.loopOut = (req, res, next) => {
max_swap_fee: req.body.swapFee,
swap_publication_deadline: req.body.swapPublicationDeadline
};
if (req.body.chanId !== '') { body['loop_out_channel'] = req.body.chanId; }
if (req.body.destAddress !== '') { body['dest'] = req.body.destAddress; }
options.body = JSON.stringify(body);
logger.info({fileName: 'Loop', msg: 'Loop Out Body: ' + options.body});
request.post(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Out: ' + JSON.stringify(body)});
if(!body || body.error) {
logger.error({fileName: 'Loop', lineNum: 28, msg: 'Loop Out Error: ' + JSON.stringify(body.error)});
if (req.body.chanId !== '') { options.body['loop_out_channel'] = req.body.chanId; }
if (req.body.destAddress !== '') { options.body['dest'] = req.body.destAddress; }
logger.info({fileName: 'Loop', msg: 'Loop Out Body: ' + JSON.stringify(options.body)});
request.post(options).then(loopOutRes => {
logger.info({fileName: 'Loop', msg: 'Loop Out: ' + JSON.stringify(loopOutRes)});
if(!loopOutRes || loopOutRes.error) {
logger.error({fileName: 'Loop', lineNum: 28, msg: 'Loop Out Error: ' + JSON.stringify(loopOutRes.error)});
res.status(500).json({
message: 'Loop Out Failed!',
error: (!body) ? 'Error From Server!' : body.error.message
error: (!loopOutRes) ? 'Error From Server!' : loopOutRes.error.message
});
} else {
res.status(201).json(body);
res.status(201).json(loopOutRes);
}
})
.catch(errRes => {
@ -51,9 +49,9 @@ exports.loopOut = (req, res, next) => {
};
exports.loopOutTerms = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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 = common.getSwapServerOptions();
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 = options.url + '/v1/loop/out/terms';
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Out Terms: ' + JSON.stringify(body)});
res.status(200).json(body);
@ -75,16 +73,15 @@ exports.loopOutTerms = (req, res, next) => {
};
exports.loopOutQuote = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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 = common.getSwapServerOptions();
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 = 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});
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Out Quote: ' + body});
body = JSON.parse(body);
body.amount = +req.params.amount;
body.swap_payment_dest = body.swap_payment_dest ? Buffer.from(body.swap_payment_dest, 'base64').toString('hex') : '';
res.status(200).json(body);
request(options).then(function (quoteRes) {
logger.info({fileName: 'Loop', msg: 'Loop Out Quote: ' + JSON.stringify(quoteRes)});
quoteRes.amount = +req.params.amount;
quoteRes.swap_payment_dest = quoteRes.swap_payment_dest ? Buffer.from(quoteRes.swap_payment_dest, 'base64').toString('hex') : '';
res.status(200).json(quoteRes);
})
.catch(errRes => {
let err = JSON.parse(JSON.stringify(errRes));
@ -103,20 +100,17 @@ exports.loopOutQuote = (req, res, next) => {
};
exports.loopOutTermsAndQuotes = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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 = common.getSwapServerOptions();
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 = options.url + '/v1/loop/out/terms';
request(options).then(function(terms) {
logger.info({fileName: 'Loop', msg: 'Loop Out Terms: ' + JSON.stringify(terms)});
const options1 = {}; const options2 = {};
terms = JSON.parse(terms);
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 = swapServerUrl + '/v1/loop/out/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
const options1 = common.getSwapServerOptions(); const options2 = common.getSwapServerOptions();
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;
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;
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)});
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[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') : '';
@ -157,14 +151,14 @@ exports.loopOutTermsAndQuotes = (req, res, next) => {
};
exports.loopIn = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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.body = JSON.stringify({
options = common.getSwapServerOptions();
if(options.url === '') { return res.status(500).json({message: "Loop In Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = options.url + '/v1/loop/in';
options.body = {
amt: req.body.amount,
max_swap_fee: req.body.swapFee,
max_miner_fee: req.body.minerFee
});
};
request.post(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In: ' + JSON.stringify(body)});
if(!body || body.error) {
@ -194,9 +188,9 @@ exports.loopIn = (req, res, next) => {
};
exports.loopInTerms = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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 = common.getSwapServerOptions();
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 = options.url + '/v1/loop/in/terms';
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In Terms: ' + JSON.stringify(body)});
res.status(200).json(body);
@ -218,13 +212,12 @@ exports.loopInTerms = (req, res, next) => {
};
exports.loopInQuote = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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 = common.getSwapServerOptions();
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 = 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});
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In Quote: ' + JSON.stringify(body)});
body = JSON.parse(body);
body.amount = +req.params.amount;
body.swap_payment_dest = body.swap_payment_dest ? Buffer.from(body.swap_payment_dest, 'base64').toString('hex') : '';
res.status(200).json(body);
@ -246,20 +239,17 @@ exports.loopInQuote = (req, res, next) => {
};
exports.loopInTermsAndQuotes = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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 = common.getSwapServerOptions();
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 = options.url + '/v1/loop/in/terms';
request(options).then(function(terms) {
logger.info({fileName: 'Loop', msg: 'Loop In Terms: ' + JSON.stringify(terms)});
const options1 = {}; const options2 = {};
terms = JSON.parse(terms);
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 = swapServerUrl + '/v1/loop/in/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
const options1 = common.getSwapServerOptions(); const options2 = common.getSwapServerOptions();
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;
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;
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)});
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[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') : '';
@ -300,12 +290,11 @@ exports.loopInTermsAndQuotes = (req, res, next) => {
};
exports.swaps = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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 = common.getSwapServerOptions();
if(options.url === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = options.url + '/v1/loop/swaps';
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Swaps: ' + body});
body = JSON.parse(body);
logger.info({fileName: 'Loop', msg: 'Loop Swaps: ' + JSON.stringify(body)});
if (body.swaps && body.swaps.length > 0) {
body.swaps.forEach(swap => {
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']) {
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({
message: "Loop Swaps Failed!",
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) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { 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 = common.getSwapServerOptions();
if(options.url === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = options.url + '/v1/loop/swap/' + req.params.id;
request(options).then(function (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.last_update_time_str = (!body.last_update_time) ? '' : common.convertTimestampToDate(Math.round(body.last_update_time/1000000000));
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();
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": {
"userPersona": "<User persona to tailor the data on UI. Allowed values MERCHANT, OPERATOR. Default MERCHANT, Required>",
"themeMode": "<Theme modes, Allowed values DAY, NIGHT. Default DAY, Required>",
"themeColor": "<Theme colors, Allowed values PURPLE, TEAL, INDIGO, PINK. 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>",
"bitcoindConfigPath": "<Path of bitcoind.conf path if available locally>",
"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"
}
},
"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": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz",
@ -7858,9 +7863,9 @@
}
},
"node-forge": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"dev": true
},
"node-libs-browser": {
@ -10761,12 +10766,12 @@
}
},
"selfsigned": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
"integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
"integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
"dev": true,
"requires": {
"node-forge": "0.9.0"
"node-forge": "^0.10.0"
}
},
"semver": {

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

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

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

@ -5,7 +5,7 @@ const authCheck = require("../authCheck");
router.get("/", authCheck, graphController.getDescribeGraph);
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("/edge/:chanid", authCheck, graphController.getGraphEdge);
router.get("/edge/:chanid/:localPubkey", authCheck, graphController.getRemoteFeePolicy);

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

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

@ -1,10 +1,10 @@
<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">
<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>
</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">
<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" />

@ -2,42 +2,41 @@
<div fxFlex="100" class="padding-gap-large">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between center" class="modal-info-header">
<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>
<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-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">
<div fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between start">
<mat-form-field fxFlex="55">
<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-form-field>
<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">
<span matSuffix> {{selAmountUnit}} </span>
<mat-error *ngIf="!transaction.satoshis">Amount is required.</mat-error>
</mat-form-field>
<mat-form-field fxFlex="10" fxLayoutAlign="start end">
<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>
<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">
<mat-form-field fxFlex="55">
<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-form-field>
<mat-form-field fxFlex="30">
<input matInput [(ngModel)]="transaction.satoshis" placeholder="Amount" name="amount" [type]="flgUseAllBalance ? 'text' : 'number'" step="100" min="0" tabindex="2" required #amount="ngModel" [disabled]="flgUseAllBalance">
<mat-hint *ngIf="flgUseAllBalance">Amount replaced by UTXO balance</mat-hint>
<span matSuffix> {{selAmountUnit}} </span>
<mat-error *ngIf="!transaction.satoshis">Amount is required.</mat-error>
</mat-form-field>
<mat-form-field fxFlex="10" fxLayoutAlign="start end">
<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-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-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>
<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 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 fxLayout="row" fxFlex="100" fxLayoutAlign="space-between stretch" *ngIf="isCompatibleVersion">
@ -75,3 +74,76 @@
</mat-card-content>
</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 { Subject, combineLatest } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';
import { takeUntil, filter, take } from 'rxjs/operators';
import { Store } from '@ngrx/store';
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 { SelNodeChild, GetInfoRoot } from '../../../shared/models/RTLconfig';
import { CLOnChainSendFunds } from '../../../shared/models/alertData';
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 { RTLConfiguration } from '../../../shared/models/RTLconfig';
import { CommonService } from '../../../shared/services/common.service';
import { LoggerService } from '../../../shared/services/logger.service';
import { RTLEffects } from '../../../store/rtl.effects';
import * as CLActions from '../../store/cl.actions';
import * as RTLActions from '../../../store/rtl.actions';
import * as fromRTLReducer from '../../../store/rtl.reducers';
import * as sha256 from 'sha256';
@Component({
selector: 'rtl-cl-on-chain-send',
@ -24,8 +31,11 @@ import * as fromRTLReducer from '../../../store/rtl.reducers';
styleUrls: ['./on-chain-send.component.scss']
})
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 sweepAll = false;
public selNode: SelNodeChild = {};
public appConfig: RTLConfiguration;
public nodeData: GetInfoRoot;
@ -51,11 +61,47 @@ export class CLOnChainSendComponent implements OnInit, OnDestroy {
public unitConversionValue = 0;
public currencyUnitFormats = CURRENCY_UNIT_FORMATS;
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() {
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(
this.store.select('root'),
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() {
if(this.invalidValues) { return true; }
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.store.dispatch(new RTLActions.OpenSpinner('Sending Funds...'));
if(this.transaction.satoshis && 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 {
if (this.sweepAll) {
this.transaction.satoshis = 'all';
this.transaction.address = this.sendFundFormGroup.controls.transactionAddress.value;
if (this.sendFundFormGroup.controls.flgMinConf.value) {
delete this.transaction.feeRate;
this.transaction.minconf = this.sendFundFormGroup.controls.transactionBlocks.value;
} else {
delete this.transaction.minconf;
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));
} 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 {
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.flgMinConf && (!this.transaction.minconf || this.transaction.minconf <= 0));
}
}
resetData() {
@ -127,6 +212,36 @@ export class CLOnChainSendComponent implements OnInit, OnDestroy {
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) {
let utxoNew = {value: 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="column" fxLayout.gt-xs="row wrap" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<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>
</div>
<mat-form-field fxFlex="30">
@ -55,10 +55,10 @@
</ng-container>
<ng-container matColumnDef="no_transaction">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

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

@ -23,10 +23,17 @@
<mat-tab label="Send">
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap">
<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>
</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>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large">
<div fxLayout="row">

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

@ -87,11 +87,11 @@
</ng-container>
<ng-container matColumnDef="no_peer">
<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>0 && (!channels.data || channels.data.length<1)">No channels available.</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>
</td>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -202,7 +202,7 @@ export class CLChannelOpenTableComponent implements OnInit, OnDestroy {
.subscribe(confirmRes => {
if (confirmRes) {
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>
</div>
</th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onChannelClick(channel, $event)">View Info</button>
</span></td>
<td mat-cell *matCellDef="let channel" fxLayoutAlign="end center" class="pr-3">
<div class="bordered-box table-actions-select">
<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 matColumnDef="no_peer">
<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>0 && (!channels.data || channels.data.length<1)">No channels available.</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>
</td>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -2,19 +2,20 @@ import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { 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 { CommonService } from '../../../../../shared/services/common.service';
import { CLChannelInformationComponent } from '../../channel-information-modal/channel-information.component';
import { CLEffects } from '../../../../store/cl.effects';
import { RTLEffects } from '../../../../../store/rtl.effects';
import * as RTLActions from '../../../../../store/rtl.actions';
import * as CLActions from '../../../../store/cl.actions';
import * as fromRTLReducer from '../../../../../store/rtl.reducers';
@Component({
@ -28,6 +29,7 @@ import * as fromRTLReducer from '../../../../../store/rtl.reducers';
export class CLChannelPendingTableComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
public isCompatibleVersion = false;
public totalBalance = 0;
public displayedColumns = [];
public channels: any;
@ -72,6 +74,9 @@ export class CLChannelPendingTableComponent implements OnInit, OnDestroy {
}
});
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.totalBalance = rtlStore.balance.totalBalance;
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) {
mychannels.sort(function(a, b) {
return (a.active === b.active) ? 0 : ((b.active) ? 1 : -1);

@ -66,7 +66,7 @@
<div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between center">
<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">
<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>
<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>

@ -60,10 +60,10 @@
</ng-container>
<ng-container matColumnDef="no_peer">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.pub_key === newlyAddedPeer && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table>

@ -60,10 +60,10 @@
</ng-container>
<ng-container matColumnDef="no_event">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -60,10 +60,10 @@
</ng-container>
<ng-container matColumnDef="no_event">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

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

@ -13,7 +13,7 @@ import { SessionService } from '../../shared/services/session.service';
import { CommonService } from '../../shared/services/common.service';
import { ErrorMessageComponent } from '../../shared/components/data-modal/error-message/error-message.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 RTLActions from '../../store/rtl.actions';
@ -37,7 +37,7 @@ export class CLEffects implements OnDestroy {
this.store.select('cl')
.pipe(takeUntil(this.unSubs[0]))
.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';
this.store.dispatch(new RTLActions.CloseSpinner());
}
@ -330,7 +330,7 @@ export class CLEffects implements OnDestroy {
closeChannelCL = this.actions$.pipe(
ofType(CLActions.CLOSE_CHANNEL_CL),
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)
.pipe(
map((postRes: any) => {

@ -73,10 +73,10 @@
</ng-container>
<ng-container matColumnDef="no_invoice">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.label == newlyAddedInvoiceMemo && row.value == newlyAddedInvoiceValue && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table>

@ -78,7 +78,7 @@
Total Attempts: {{payment?.total_parts}}
</span>
<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 yellow mt-0" matTooltip="Incomplete/Failed" matTooltipPosition="right" [ngClass]="{'mr-0': screenSize === screenSizeEnum.XS}"></span>
{{mpp.created_at_str}}
@ -130,7 +130,7 @@
</ng-container>
<tr mat-row *matRowDef="let row; columns: mppColumns; 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-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-row *matRowDef="let row; columns: displayedColumns; when: !is_group;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table>

@ -25,3 +25,7 @@
.mpp-row-span {
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.paymentDecoded = decodedPayment;
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]))
.subscribe(data => {
if(this.selNode.fiatConversion) {

@ -101,7 +101,7 @@ export class CLLightningSendPaymentsComponent implements OnInit, OnDestroy {
this.paymentDecodedHint = 'Zero Amount Invoice | Memo: ' + this.paymentDecoded.description;
} else {
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]))
.subscribe(data => {
if(this.selNode.fiatConversion) {
@ -148,7 +148,7 @@ export class CLLightningSendPaymentsComponent implements OnInit, OnDestroy {
this.paymentDecodedHint = 'Zero Amount Invoice | Memo: ' + this.paymentDecoded.description;
} else {
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]))
.subscribe(data => {
if(this.selNode.fiatConversion) {

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

@ -52,10 +52,10 @@
</ng-container>
<ng-container matColumnDef="no_transaction">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -41,10 +41,10 @@
</ng-container>
<ng-container matColumnDef="no_channel">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -65,11 +65,11 @@
</ng-container>
<ng-container matColumnDef="no_peer">
<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>0 && (!channels.data || channels.data.length<1)">No open channels available.</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>
</td>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -130,18 +130,21 @@ export class ECLChannelOpenTableComponent implements OnInit, OnDestroy {
}
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: {
type: AlertTypeEnum.CONFIRM,
alertTitle: 'Close Channel',
titleMessage: 'Closing channel: ' + channelToClose.channelId,
alertTitle: alertTitle,
titleMessage: titleMessage,
noBtnText: 'Cancel',
yesBtnText: 'Close Channel'
yesBtnText: yesBtnText
}}));
this.rtlEffects.closeConfirm
.pipe(takeUntil(this.unSubs[3]))
.subscribe(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}));
}
});

@ -41,10 +41,10 @@
</ng-container>
<ng-container matColumnDef="no_channel">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -66,10 +66,10 @@
</ng-container>
<ng-container matColumnDef="no_peer">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.pub_key === newlyAddedPeer && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table>

@ -15,13 +15,17 @@
<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>
</ng-container>
<ng-container matColumnDef="amountOut">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Amount Out (Sats)</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent?.amountOut | number}}</span></td>
<ng-container matColumnDef="fee">
<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?.amountIn - fhEvent?.amountOut) | number}}</span></td>
</ng-container>
<ng-container matColumnDef="paymentHash">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Payment Hash</th>
<td mat-cell *matCellDef="let fhEvent" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '30rem'}">{{fhEvent?.paymentHash}}</td>
<ng-container matColumnDef="fromAlias">
<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?.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 matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3">
@ -38,10 +42,10 @@
</ng-container>
<ng-container matColumnDef="no_event">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -1,7 +1,16 @@
.mat-column-paymentHash {
.mat-column-fromAlias {
padding-left: 2rem;
flex: 1 1 25%;
width: 25%;
flex: 1 1 20%;
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;
overflow: hidden;
text-overflow: ellipsis;

@ -39,10 +39,10 @@ export class ECLForwardingHistoryComponent implements OnInit, OnChanges {
this.displayedColumns = ['timestamp', 'amountIn', 'actions'];
} else if(this.screenSize === ScreenSizeEnum.SM || this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['timestamp', 'amountIn', 'amountOut', 'actions'];
this.displayedColumns = ['timestamp', 'amountIn', 'fee', 'actions'];
} else {
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) {
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: '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: '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}]
];
this.store.dispatch(new RTLActions.OpenAlert({ data: {

@ -29,7 +29,7 @@ export class ECLRoutingComponent implements OnInit, OnDestroy {
.subscribe((rtlStore) => {
this.errorMessage = '';
rtlStore.effectErrors.forEach(effectsErr => {
if (effectsErr.action === 'FetchAudit') {
if (effectsErr.action === 'FetchPayments') {
this.flgLoading[0] = 'error';
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 FETCH_INFO_ECL = 'FETCH_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 FETCH_CHANNELS_ECL = 'FETCH_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 CLOSE_CHANNEL_ECL = 'CLOSE_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 GET_QUERY_ROUTES_ECL = 'GET_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_STATUS_ECL = 'SEND_PAYMENT_STATUS_ECL';
export const FETCH_TRANSACTIONS_ECL = 'FETCH_TRANSACTIONS_ECL';
@ -85,8 +84,8 @@ export class SetInfo implements Action {
constructor(public payload: GetInfo) {}
}
export class FetchAudit implements Action {
readonly type = FETCH_AUDIT_ECL;
export class FetchFees implements Action {
readonly type = FETCH_FEES_ECL;
}
export class SetFees implements Action {
@ -96,6 +95,7 @@ export class SetFees implements Action {
export class FetchChannels implements Action {
readonly type = FETCH_CHANNELS_ECL;
constructor(public payload: {fetchPayments: boolean}) {}
}
export class SetActiveChannels implements Action {
@ -204,6 +204,10 @@ export class RemoveChannel implements Action {
constructor(public payload: {channelId: string}) {}
}
export class FetchPayments implements Action {
readonly type = FETCH_PAYMENTS_ECL;
}
export class SetPayments implements Action {
readonly type = SET_PAYMENTS_ECL;
constructor(public payload: Payments) {}
@ -219,16 +223,6 @@ export class SetQueryRoutes implements Action {
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 {
readonly type = SEND_PAYMENT_ECL;
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 |
FetchInfo | SetInfo | FetchAudit | SetFees |
FetchInfo | SetInfo | FetchFees | SetFees |
FetchChannels | SetActiveChannels | SetPendingChannels | SetInactiveChannels |
FetchPeers | SetPeers | AddPeer | DisconnectPeer | SaveNewPeer | RemovePeer | NewlyAddedPeer |
SetChannelsStatus | FetchChannelStats | SetChannelStats |
@ -296,5 +290,5 @@ export type ECLActions = ResetECLStore | ClearEffectError | EffectError | SetChi
SendOnchainFunds | SendOnchainFundsRes | FetchTransactions | SetTransactions |
SetLightningBalance | FetchPeers | SetPeers | PeerLookup | SetLookup |
SaveNewChannel | UpdateChannels | CloseChannel | RemoveChannel |
SetPayments | DecodePayment | SetDecodedPayment | SendPayment | SendPaymentStatus |
FetchPayments | SetPayments | SendPayment | SendPaymentStatus |
FetchInvoices | SetInvoices | CreateInvoice | AddInvoice;

@ -1,5 +1,5 @@
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 { Store } from '@ngrx/store';
import { Actions, Effect, ofType } from '@ngrx/effects';
@ -36,7 +36,7 @@ export class ECLEffects implements OnDestroy {
this.store.select('ecl')
.pipe(takeUntil(this.unSubs[0]))
.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';
this.store.dispatch(new RTLActions.CloseSpinner());
}
@ -72,23 +72,44 @@ export class ECLEffects implements OnDestroy {
));
@Effect()
fetchAudit = this.actions$.pipe(
ofType(ECLActions.FETCH_AUDIT_ECL),
mergeMap((action: ECLActions.FetchAudit) => {
this.store.dispatch(new ECLActions.ClearEffectError('FetchAudit'));
return this.httpClient.get<Audit>(this.CHILD_API_URL + environment.FEES_API);
}),
map((audit: Audit) => {
this.logger.info(audit);
this.store.dispatch(new ECLActions.SetPayments(audit.payments));
return {
type: ECLActions.SET_FEES_ECL,
payload: audit && audit.fees ? audit.fees : {}
};
}),
catchError((err: any) => {
this.handleErrorWithoutAlert('FetchAudit', 'Fetching Fees Failed.', err);
return of({type: RTLActions.VOID});
fetchFees = this.actions$.pipe(
ofType(ECLActions.FETCH_FEES_ECL),
mergeMap((action: ECLActions.FetchFees) => {
this.store.dispatch(new ECLActions.ClearEffectError('FetchFees'));
return this.httpClient.get<Audit>(this.CHILD_API_URL + environment.FEES_API + '/fees')
.pipe(map((fees: any) => {
this.logger.info(fees);
return {
type: ECLActions.SET_FEES_ECL,
payload: fees ? fees : {}
};
},
catchError((err: any) => {
this.handleErrorWithoutAlert('FetchFees', '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.SetInactiveChannels((channelsRes && channelsRes.inactiveChannels.length > 0) ? channelsRes.inactiveChannels : []));
this.store.dispatch(new ECLActions.SetLightningBalance(channelsRes.lightningBalances));
if (action.payload.fetchPayments) {
this.store.dispatch(new ECLActions.FetchPayments());
}
return {
type: ECLActions.SET_CHANNELS_STATUS_ECL,
payload: channelsRes.channelStatus
@ -220,7 +244,7 @@ export class ECLEffects implements OnDestroy {
map((postRes: Peer[]) => {
this.logger.info(postRes);
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 {
type: ECLActions.NEWLY_ADDED_PEER_ECL,
payload: { peer: postRes[0] }
@ -327,7 +351,7 @@ export class ECLEffects implements OnDestroy {
this.logger.info(postRes);
setTimeout(() => {
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!'));
}, 2000);
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()
sendPayment = this.actions$.pipe(
ofType(ECLActions.SEND_PAYMENT_ECL),
@ -432,9 +420,8 @@ export class ECLEffects implements OnDestroy {
setTimeout(() => {
this.store.dispatch(new ECLActions.SendPaymentStatus(sendRes));
this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new ECLActions.FetchChannels());
this.store.dispatch(new ECLActions.SetDecodedPayment({}));
this.store.dispatch(new ECLActions.FetchAudit());
this.store.dispatch(new ECLActions.FetchChannels({fetchPayments: true}));
this.store.dispatch(new ECLActions.FetchPayments());
this.store.dispatch(new RTLActions.OpenSnackBar('Payment Submitted!'));
}, 3000);
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.SetNodeData(node_data));
this.store.dispatch(new ECLActions.FetchAudit());
this.store.dispatch(new ECLActions.FetchChannels());
this.store.dispatch(new ECLActions.FetchChannels({fetchPayments: true}));
this.store.dispatch(new ECLActions.FetchFees());
this.store.dispatch(new ECLActions.FetchOnchainBalance());
this.store.dispatch(new ECLActions.FetchPeers());
let newRoute = this.location.path();

@ -156,7 +156,30 @@ export function ECLReducer(state = initECLState, action: ECLActions.ECLActions)
activeChannels: modifiedChannels
};
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 {
...state,
initialAPIResponseStatus: newAPIStatus,

@ -16,15 +16,25 @@
<qrcode [qrdata]="invoice.serialized" [margin]="2" [width]="qrWidth" [errorCorrectionLevel]="'L'" [allowEmptyString]="true"></qrcode>
</div>
<div fxLayout="row">
<div fxFlex="40">
<h4 fxLayoutAlign="start" class="font-bold-500">{{screenSize === screenSizeEnum.XS ? 'Amount' : 'Amount Requested'}}</h4>
<div fxFlex="50">
<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>
</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>
<span class="overflow-wrap foreground-secondary-text">{{invoice.timestampStr}}</span>
</div>
<div fxFlex="20">
<div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Status</h4>
<span class="overflow-wrap foreground-secondary-text">{{invoice.status | titlecase}}</span>
</div>

@ -53,8 +53,15 @@
</ng-container>
<ng-container matColumnDef="amount">
<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'}}
</span></td>
<td mat-cell *matCellDef="let invoice" class="pr-3">
<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 matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3">
@ -71,10 +78,10 @@
</ng-container>
<ng-container matColumnDef="no_invoice">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.label == newlyAddedInvoiceMemo && row.value == newlyAddedInvoiceValue && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table>

@ -64,13 +64,13 @@ export class ECLLightningInvoicesComponent implements OnInit, OnDestroy {
this.displayedColumns = ['timestamp', 'amount', 'actions'];
} else if(this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false;
this.displayedColumns = ['timestamp', 'description', 'amount', 'actions'];
this.displayedColumns = ['timestamp', 'amount', 'amountSettled', 'actions'];
} else if(this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['timestamp', 'description', 'amount', 'actions'];
this.displayedColumns = ['timestamp', 'amount', 'amountSettled', 'actions'];
} else {
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>
<div fxLayout="row">
<div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Recipient Node ID</h4>
<span class="foreground-secondary-text">{{payment.recipientNodeId}}</span>
<h4 fxLayoutAlign="start" class="font-bold-500">Recipient Node</h4>
<span class="foreground-secondary-text">{{payment.recipientNodeAlias}}</span>
</div>
</div>
<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 fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Parts</h4>
<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-panel-title>
<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>
<div fxLayout="row">
<div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">To Channel ID</h4>
<span class="overflow-wrap foreground-secondary-text">{{part.toChannelId}}</span>
<h4 fxLayoutAlign="start" class="font-bold-500">To Channel</h4>
<span class="overflow-wrap foreground-secondary-text">{{part.toChannelAlias}}</span>
</div>
</div>
</div>
@ -90,7 +96,7 @@
</div>
</div>
</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()">
<mat-icon fxLayoutAlign="center center">arrow_downward</mat-icon>
</button>

@ -12,13 +12,17 @@ import { ECLPaymentInformation } from '../../../shared/models/alertData';
export class ECLPaymentInformationComponent implements OnInit, AfterViewChecked {
@ViewChild('scrollContainer', { static: true }) scrollContainer: ElementRef;
public payment: PaymentSent;
public description: string = null;
public shouldScroll = true;
public expansionOpen = false;
public expansionOpen = true;
constructor(public dialogRef: MatDialogRef<ECLPaymentInformationComponent>, @Inject(MAT_DIALOG_DATA) public data: ECLPaymentInformation) { }
ngOnInit() {
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() {

@ -32,11 +32,19 @@
</ng-container>
<ng-container matColumnDef="id">
<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 matColumnDef="recipientNodeId">
<ng-container matColumnDef="recipientNodeAlias">
<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 matColumnDef="recipientAmount">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Amount (Sats)</th>
@ -52,15 +60,82 @@
</div>
</th>
<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>
</ng-container>
<ng-container matColumnDef="no_payment">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'"></tr>
</table>

@ -1,10 +1,36 @@
.mat-column-id, .mat-column-recipientNodeId, .mat-column-paymentHash {
flex: 1 1 20%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.mat-column-id, .mat-column-recipientNodeAlias,
.mat-column-groupId, .mat-column-groupChannelAlias {
padding: 0 1rem;
flex: 0 0 25%;
width: 25%;
& .ellipsis-parent {
display: flex;
}
}
.mat-column-actions {
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 { DecimalPipe, TitleCasePipe } from '@angular/common';
import { Subject } from 'rxjs';
import { forkJoin, Subject } from 'rxjs';
import { takeUntil, take } from 'rxjs/operators';
import { Store } from '@ngrx/store';
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 { MatSort } from '@angular/material/sort';
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 { LoggerService } from '../../../shared/services/logger.service';
import { CommonService } from '../../../shared/services/common.service';
import { DataService } from '../../../shared/services/data.service';
import { newlyAddedRowAnimation } from '../../../shared/animation/row-animation';
import { ECLLightningSendPaymentsComponent } from '../send-payment-modal/send-payment.component';
@ -47,6 +48,7 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
public paymentJSONArr: PaymentSent[] = [];
public paymentDecoded: PayRequest = {};
public displayedColumns = [];
public partColumns = [];
public paymentRequest = '';
public paymentDecodedHint = '';
public flgSticky = false;
@ -56,20 +58,24 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
public screenSizeEnum = ScreenSizeEnum;
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();
if(this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
this.displayedColumns = ['firstPartTimestamp', 'actions'];
this.partColumns = ['groupTotal', 'groupAction'];
} else if(this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false;
this.displayedColumns = ['firstPartTimestamp', 'recipientAmount', 'actions'];
this.partColumns = ['groupTotal', 'groupAmount', 'groupAction'];
} else if(this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['firstPartTimestamp', 'id', 'recipientAmount', 'actions'];
this.partColumns = ['groupTotal', 'groupId', 'groupAmount', 'groupAction'];
} else {
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]))
.subscribe((rtlStore) => {
rtlStore.effectErrors.forEach(effectsErr => {
if (effectsErr.action === 'FetchAudit') {
if (effectsErr.action === 'FetchPayments') {
this.flgLoading[0] = 'error';
}
});
this.information = rtlStore.information;
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.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.sort = this.sort;
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) {
this.sendPayment();
} else {
this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...'));
this.store.dispatch(new ECLActions.DecodePayment({routeParam: this.paymentRequest, fromDialog: false}));
this.eclEffects.setDecodedPayment.pipe(take(1))
.subscribe(decodedPayment => {
this.dataService.decodePayment(this.paymentRequest, false)
.pipe(take(1)).subscribe((decodedPayment: PayRequest) => {
this.paymentDecoded = decodedPayment;
if (this.paymentDecoded.timestamp) {
if (!this.paymentDecoded.amount) {
@ -189,9 +210,8 @@ export class ECLLightningPaymentsComponent implements OnInit, OnDestroy {
this.paymentRequest = event;
this.paymentDecodedHint = '';
if(this.paymentRequest && this.paymentRequest.length > 100) {
this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...'));
this.store.dispatch(new ECLActions.DecodePayment({routeParam: this.paymentRequest, fromDialog: false}));
this.eclEffects.setDecodedPayment.subscribe(decodedPayment => {
this.dataService.decodePayment(this.paymentRequest, false)
.pipe(take(1)).subscribe((decodedPayment: PayRequest) => {
this.paymentDecoded = decodedPayment;
if(this.paymentDecoded.amount) {
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();
}
onPaymentClick(selPayment: PaymentSent, event: any) {
this.store.dispatch(new RTLActions.OpenAlert({ data: {
is_group(index: number, payment: PaymentSent) {
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,
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) {
this.payments.filter = selFilter;
}
onDownloadCSV() {
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() {

@ -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 { CommonService } from '../../../shared/services/common.service';
import { LoggerService } from '../../../shared/services/logger.service';
import { DataService } from '../../../shared/services/data.service';
import { ECLEffects } from '../../store/ecl.effects';
import * as ECLActions from '../../store/ecl.actions';
@ -41,7 +42,7 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
public paymentError = '';
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() {
this.store.select('ecl')
@ -62,10 +63,6 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
delete this.paymentDecoded.amount;
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.paymentDecodedHint = '';
this.paymentReq.control.setErrors(null);
this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...'));
this.store.dispatch(new ECLActions.DecodePayment({routeParam: this.paymentRequest, fromDialog: true}));
this.eclEffects.setDecodedPayment.pipe(take(1)).subscribe(decodedPayment => {
this.dataService.decodePayment(this.paymentRequest, true)
.pipe(take(1)).subscribe((decodedPayment: PayRequest) => {
this.paymentDecoded = decodedPayment;
if (this.paymentDecoded.timestamp && !this.paymentDecoded.amount) {
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) {
this.paymentReq.control.setErrors(null);
this.zeroAmtInvoice = false;
this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...'));
this.store.dispatch(new ECLActions.DecodePayment({routeParam: this.paymentRequest, fromDialog: true}));
this.eclEffects.setDecodedPayment.subscribe(decodedPayment => {
this.dataService.decodePayment(this.paymentRequest, true)
.pipe(take(1)).subscribe((decodedPayment: PayRequest) => {
this.paymentDecoded = decodedPayment;
if (this.paymentDecoded.timestamp && !this.paymentDecoded.amount) {
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 matColumnDef="no_channel">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -5,11 +5,11 @@
<button mat-flat-button color="primary" tabindex="1" (click)="onRestoreChannels({})">Restore All</button>
</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" 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 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>
</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">
@ -35,10 +35,10 @@
</ng-container>
<ng-container matColumnDef="no_channel">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -22,10 +22,10 @@
<mat-card-content class="dashboard-card-content" fxLayout="column" fxFlex="95">
<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-balances-info fxFlex="100" *ngSwitchCase="'balance'" [balances]="balances" [ngClass]="{'error-border': flgLoading[2]==='error' || flgLoading[5]==='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-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[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-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>
</div>
</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}}">
<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-balances-info fxFlex="100" *ngSwitchCase="'balance'" [balances]="balances" [ngClass]="{'error-border': flgLoading[2]==='error' || flgLoading[5]==='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="'outboundLiq'" [direction]="'Out'" [totalLiquidity]="totalOutboundLiquidity" [allChannels]="allOutboundChannels" [ngClass]="{'error-border': flgLoading[5]==='error'}"></rtl-channel-liquidity-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[3]==='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'">
<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>

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

@ -12,7 +12,9 @@ import { PeersComponent } from './peers-channels/peers/peers.component';
import { LightningInvoicesComponent } from './transactions/invoices/lightning-invoices.component';
import { OnChainReceiveComponent } from './on-chain/on-chain-receive/on-chain-receive.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 { LightningPaymentsComponent } from './transactions/payments/lightning-payments.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 { QueryRoutesComponent } from './transactions/query-routes/query-routes.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 { InitializeWalletComponent } from './wallet/initialize/initialize.component';
import { NodeInfoComponent } from './home/node-info/node-info.component';
@ -78,9 +81,12 @@ import { LNDUnlockedGuard } from '../shared/services/auth.guard';
QueryRoutesComponent,
OnChainReceiveComponent,
OnChainComponent,
UTXOTablesComponent,
OnChainUTXOsComponent,
OnChainTransactionHistoryComponent,
ChannelsTablesComponent,
ChannelOpenTableComponent,
ChannelActiveHTLCsTableComponent,
UnlockWalletComponent,
InitializeWalletComponent,
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>
<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.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>
</form>
</mat-step>

@ -114,14 +114,14 @@ export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
}
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.stepper.selected.stepControl.setErrors(null);
this.stepper.next();
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]))
.subscribe((loopStatus: any) => {
this.loopStatus = JSON.parse(loopStatus);
this.loopStatus = loopStatus;
this.store.dispatch(new LNDActions.FetchLoopSwaps());
this.flgEditable = true;
}, (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);
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) => {
this.loopStatus = JSON.parse(loopStatus);
this.loopStatus = loopStatus;
this.store.dispatch(new LNDActions.FetchLoopSwaps());
this.flgEditable = true;
}, (err) => {
@ -148,8 +148,6 @@ export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
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; }
this.stepper.selected.stepControl.setErrors(null);
this.stepper.next();
this.store.dispatch(new RTLActions.OpenSpinner('Getting Quotes...'));
let swapPublicationDeadline = this.inputFormGroup.controls.fast.value ? 0 : new Date().getTime() + (30 * 60000);
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.stepper.selected.stepControl.setErrors(null);
this.stepper.next();
}
stepSelectionChanged(event: any) {
@ -252,7 +252,17 @@ export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
onStepChanged(index: number) {
this.animationDirection = index < this.stepNumber ? 'backward' : 'forward';
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() {
this.unSubs.forEach(completeSub => {

@ -71,10 +71,10 @@
</ng-container>
<ng-container matColumnDef="no_swap">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -38,6 +38,7 @@ export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
public storedSwaps: SwapStatus[] = [];
public filteredSwaps: SwapStatus[] = [];
public flgLoading: Array<Boolean | 'error'> = [true];
public emptyTableMessage = 'No swaps available.';
public flgSticky = false;
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
@ -68,7 +69,10 @@ export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
.pipe(takeUntil(this.unSubs[1]))
.subscribe((rtlStore) => {
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) {
this.storedSwaps = rtlStore.loopSwaps;
@ -76,7 +80,7 @@ export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
this.loadSwapsTable(this.filteredSwaps);
}
if (this.flgLoading[0] !== 'error') {
this.flgLoading[0] = ( rtlStore.transactions) ? false : true;
this.flgLoading[0] = (rtlStore.loopSwaps) ? false : true;
}
this.logger.info(rtlStore);
});
@ -84,7 +88,8 @@ export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
}
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.loadSwapsTable(this.filteredSwaps);
}

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

@ -31,14 +31,12 @@
<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>
</div>
</div>
</mat-tab>
</mat-tab-group>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large">
<div fxLayout="row">
<rtl-on-chain-transaction-history fxLayout="row" fxFlex="100"></rtl-on-chain-transaction-history>
</div>
<rtl-utxo-tables fxLayout="row" fxFlex="100"></rtl-utxo-tables>
</div>
</mat-card-content>
</mat-card>

@ -2,7 +2,6 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
import { faExchangeAlt, faChartPie } from '@fortawesome/free-solid-svg-icons';
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}];
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() {
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="column" fxLayout.gt-xs="row wrap" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70">
<fa-icon [icon]="faHistory" class="page-title-img mr-1"></fa-icon>
<span class="page-title">Transaction History</span>
</div>
<div fxFlex="70"></div>
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutAlign="start start">
<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
[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">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Date/Time </th>
<td mat-cell *matCellDef="let transaction">{{transaction.time_stamp_str}}</td>
@ -52,10 +49,10 @@
</ng-container>
<ng-container matColumnDef="no_transaction">
<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>
</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-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

@ -1,6 +1,4 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';
import { Component, ViewChild, Input, OnChanges } from '@angular/core';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
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 { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Transaction } from '../../../shared/models/lndModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum } from '../../../shared/services/consts-enums-functions';
import { LoggerService } from '../../../shared/services/logger.service';
import { CommonService } from '../../../shared/services/common.service';
import { Transaction } from '../../../../shared/models/lndModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum } from '../../../../shared/services/consts-enums-functions';
import { LoggerService } from '../../../../shared/services/logger.service';
import { CommonService } from '../../../../shared/services/common.service';
import * as LNDActions from '../../store/lnd.actions';
import * as RTLActions from '../../../store/rtl.actions';
import * as fromRTLReducer from '../../../store/rtl.reducers';
import * as RTLActions from '../../../../store/rtl.actions';
import * as fromRTLReducer from '../../../../store/rtl.reducers';
@Component({
selector: 'rtl-on-chain-transaction-history',
@ -25,19 +22,19 @@ import * as fromRTLReducer from '../../../store/rtl.reducers';
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Transactions') }
]
})
export class OnChainTransactionHistoryComponent implements OnInit, OnDestroy {
export class OnChainTransactionHistoryComponent implements OnChanges {
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
@Input() transactions: Transaction[];
@Input() errorLoading: any;
faHistory = faHistory;
public displayedColumns = [];
public listTransactions: any;
public flgLoading: Array<Boolean | 'error'> = [true];
public flgSticky = false;
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = '';
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) {
this.screenSize = this.commonService.getScreenSize();
@ -56,25 +53,10 @@ export class OnChainTransactionHistoryComponent implements OnInit, OnDestroy {
}
}
ngOnInit() {
this.store.dispatch(new LNDActions.FetchTransactions());
this.store.select('lnd')
.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);
});
ngOnChanges() {
if (this.transactions && this.transactions.length > 0) {
this.loadTransactionsTable(this.transactions);
}
}
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