Eclair Websocket updates for payment and channel events (#840)

pull/834/head
ShahanaFarooqui 3 years ago committed by GitHub
parent a906c46405
commit a41a7ba7f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -10,9 +10,9 @@
<link i18n-rel="" rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5">
<meta i18n-content="" name="msapplication-TileColor" content="#da532c">
<meta i18n-content="" name="theme-color" content="#ffffff">
<style>@font-face{font-family:Roboto;src:url(Roboto-Thin.dbd56bd3357dc3617fe5.woff2) format("woff2"),url(Roboto-Thin.e7f7c82374bd0ebef14b.woff) format("woff");font-weight:100;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.a8cef84f735ef887abdc.woff2) format("woff2"),url(Roboto-ThinItalic.5dd9349c940073834e9a.woff) format("woff");font-weight:100;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Light.c27d89ac77468ae18f28.woff2) format("woff2"),url(Roboto-Light.d923dfafc0c5183b59aa.woff) format("woff");font-weight:300;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.506274c7228cf81cae4d.woff2) format("woff2"),url(Roboto-LightItalic.d4b8c137518d9d92bb28.woff) format("woff");font-weight:300;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Regular.64cfb66c866ea50cad47.woff2) format("woff2"),url(Roboto-Regular.e02e9d6ff5547f7e9962.woff) format("woff");font-weight:400;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.4dd2af1e8df532f41db8.woff2) format("woff2"),url(Roboto-RegularItalic.5ea38fff9eebef99c5df.woff) format("woff");font-weight:400;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Medium.1d3bced88509b0838984.woff2) format("woff2"),url(Roboto-Medium.092c6130df8fd2199888.woff) format("woff");font-weight:500;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.d620b8f53f75966fe42e.woff2) format("woff2"),url(Roboto-MediumItalic.18ff1628c628080166c1.woff) format("woff");font-weight:500;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Bold.92fbd4e93cf0a5dbebaa.woff2) format("woff2"),url(Roboto-Bold.73288d91c325e82a5b92.woff) format("woff");font-weight:700;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.5f600d98a73d800ae575.woff2) format("woff2"),url(Roboto-BoldItalic.6d89acbd21d7e3fbecb2.woff) format("woff");font-weight:700;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Black.41ed1105a6ebb8ffe34e.woff2) format("woff2"),url(Roboto-Black.937491dfcbe64ca9a9f1.woff) format("woff");font-weight:900;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.50ca4c51ebc27e7e7d2f.woff2) format("woff2"),url(Roboto-BlackItalic.2e1ee657996854c6f427.woff) format("woff");font-weight:900;font-style:italic;}html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:62.5%;}body{box-sizing:border-box;margin:0;}body{height:100%;overflow:hidden;}*{margin:0;padding:0;}</style><link rel="stylesheet" href="styles.25fb6eaa0bedeee33fea.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.25fb6eaa0bedeee33fea.css"></noscript></head>
<style>@font-face{font-family:Roboto;src:url(Roboto-Thin.dbd56bd3357dc3617fe5.woff2) format("woff2"),url(Roboto-Thin.e7f7c82374bd0ebef14b.woff) format("woff");font-weight:100;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.a8cef84f735ef887abdc.woff2) format("woff2"),url(Roboto-ThinItalic.5dd9349c940073834e9a.woff) format("woff");font-weight:100;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Light.c27d89ac77468ae18f28.woff2) format("woff2"),url(Roboto-Light.d923dfafc0c5183b59aa.woff) format("woff");font-weight:300;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.506274c7228cf81cae4d.woff2) format("woff2"),url(Roboto-LightItalic.d4b8c137518d9d92bb28.woff) format("woff");font-weight:300;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Regular.64cfb66c866ea50cad47.woff2) format("woff2"),url(Roboto-Regular.e02e9d6ff5547f7e9962.woff) format("woff");font-weight:400;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.4dd2af1e8df532f41db8.woff2) format("woff2"),url(Roboto-RegularItalic.5ea38fff9eebef99c5df.woff) format("woff");font-weight:400;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Medium.1d3bced88509b0838984.woff2) format("woff2"),url(Roboto-Medium.092c6130df8fd2199888.woff) format("woff");font-weight:500;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.d620b8f53f75966fe42e.woff2) format("woff2"),url(Roboto-MediumItalic.18ff1628c628080166c1.woff) format("woff");font-weight:500;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Bold.92fbd4e93cf0a5dbebaa.woff2) format("woff2"),url(Roboto-Bold.73288d91c325e82a5b92.woff) format("woff");font-weight:700;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.5f600d98a73d800ae575.woff2) format("woff2"),url(Roboto-BoldItalic.6d89acbd21d7e3fbecb2.woff) format("woff");font-weight:700;font-style:italic;}@font-face{font-family:Roboto;src:url(Roboto-Black.41ed1105a6ebb8ffe34e.woff2) format("woff2"),url(Roboto-Black.937491dfcbe64ca9a9f1.woff) format("woff");font-weight:900;font-style:normal;}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.50ca4c51ebc27e7e7d2f.woff2) format("woff2"),url(Roboto-BlackItalic.2e1ee657996854c6f427.woff) format("woff");font-weight:900;font-style:italic;}html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:62.5%;}body{box-sizing:border-box;margin:0;}body{height:100%;overflow:hidden;}*{margin:0;padding:0;}</style><link rel="stylesheet" href="styles.3d3ee2a46a070a00611c.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.3d3ee2a46a070a00611c.css"></noscript></head>
<body>
<rtl-app></rtl-app>
<script src="runtime.96d7358328212eeabd62.js" defer></script><script src="polyfills.a979cbbe16939013cdcf.js" defer></script><script src="main.a49d7e8de0a91e20b28f.js" defer></script>
<script src="runtime.bc9d4fb7b8e0d4c34804.js" defer></script><script src="polyfills.a979cbbe16939013cdcf.js" defer></script><script src="main.30bd685dd818a8706991.js" defer></script>
</body></html>

File diff suppressed because one or more lines are too long

@ -1 +1 @@
(()=>{"use strict";var e,r,t,o={},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=o,e=[],n.O=(r,t,o,a)=>{if(!t){var l=1/0;for(s=0;s<e.length;s++){for(var[t,o,a]=e[s],i=!0,d=0;d<t.length;d++)(!1&a||l>=a)&&Object.keys(n.O).every(e=>n.O[e](t[d]))?t.splice(d--,1):(i=!1,a<l&&(l=a));i&&(e.splice(s--,1),r=o())}return r}a=a||0;for(var s=e.length;s>0&&e[s-1][2]>a;s--)e[s]=e[s-1];e[s]=[t,o,a]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((r,t)=>(n.f[t](e,r),r),[])),n.u=e=>e+"."+{145:"118ca353c539b86870f5",432:"9db359c61436bdcbc11f",891:"b3aa0591a52b13f0db13",958:"16231e91ce5e6e57c552"}[e]+".js",n.miniCssF=e=>"styles.25fb6eaa0bedeee33fea.css",n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="rtl:",n.l=(e,o,a,l)=>{if(r[e])r[e].push(o);else{var i,d;if(void 0!==a)for(var s=document.getElementsByTagName("script"),u=0;u<s.length;u++){var c=s[u];if(c.getAttribute("src")==e||c.getAttribute("data-webpack")==t+a){i=c;break}}i||(d=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,n.nc&&i.setAttribute("nonce",n.nc),i.setAttribute("data-webpack",t+a),i.src=e),r[e]=[o];var f=(t,o)=>{i.onerror=i.onload=null,clearTimeout(p);var a=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),a&&a.forEach(e=>e(o)),t)return t(o)},p=setTimeout(f.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=f.bind(null,i.onerror),i.onload=f.bind(null,i.onload),d&&document.head.appendChild(i)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.p="",(()=>{var e={666:0};n.f.j=(r,t)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else if(666!=r){var a=new Promise((t,a)=>o=e[r]=[t,a]);t.push(o[2]=a);var l=n.p+n.u(r),i=new Error;n.l(l,t=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var a=t&&("load"===t.type?"missing":t.type),l=t&&t.target&&t.target.src;i.message="Loading chunk "+r+" failed.\n("+a+": "+l+")",i.name="ChunkLoadError",i.type=a,i.request=l,o[1](i)}},"chunk-"+r,r)}else e[r]=0},n.O.j=r=>0===e[r];var r=(r,t)=>{var o,a,[l,i,d]=t,s=0;for(o in i)n.o(i,o)&&(n.m[o]=i[o]);if(d)var u=d(n);for(r&&r(t);s<l.length;s++)n.o(e,a=l[s])&&e[a]&&e[a][0](),e[l[s]]=0;return n.O(u)},t=self.webpackChunkrtl=self.webpackChunkrtl||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})()})();
(()=>{"use strict";var e,r,t,o={},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,n),t.loaded=!0,t.exports}n.m=o,e=[],n.O=(r,t,o,a)=>{if(!t){var l=1/0;for(s=0;s<e.length;s++){for(var[t,o,a]=e[s],d=!0,i=0;i<t.length;i++)(!1&a||l>=a)&&Object.keys(n.O).every(e=>n.O[e](t[i]))?t.splice(i--,1):(d=!1,a<l&&(l=a));d&&(e.splice(s--,1),r=o())}return r}a=a||0;for(var s=e.length;s>0&&e[s-1][2]>a;s--)e[s]=e[s-1];e[s]=[t,o,a]},n.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce((r,t)=>(n.f[t](e,r),r),[])),n.u=e=>e+"."+{145:"162f4fba7559fca28c13",432:"214076a4aec175d96522",891:"d267f212990dff4dce6e",958:"b484cd840add91e5a7b2"}[e]+".js",n.miniCssF=e=>"styles.3d3ee2a46a070a00611c.css",n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="rtl:",n.l=(e,o,a,l)=>{if(r[e])r[e].push(o);else{var d,i;if(void 0!==a)for(var s=document.getElementsByTagName("script"),u=0;u<s.length;u++){var c=s[u];if(c.getAttribute("src")==e||c.getAttribute("data-webpack")==t+a){d=c;break}}d||(i=!0,(d=document.createElement("script")).charset="utf-8",d.timeout=120,n.nc&&d.setAttribute("nonce",n.nc),d.setAttribute("data-webpack",t+a),d.src=e),r[e]=[o];var f=(t,o)=>{d.onerror=d.onload=null,clearTimeout(p);var a=r[e];if(delete r[e],d.parentNode&&d.parentNode.removeChild(d),a&&a.forEach(e=>e(o)),t)return t(o)},p=setTimeout(f.bind(null,void 0,{type:"timeout",target:d}),12e4);d.onerror=f.bind(null,d.onerror),d.onload=f.bind(null,d.onload),i&&document.head.appendChild(d)}},n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.p="",(()=>{var e={666:0};n.f.j=(r,t)=>{var o=n.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else if(666!=r){var a=new Promise((t,a)=>o=e[r]=[t,a]);t.push(o[2]=a);var l=n.p+n.u(r),d=new Error;n.l(l,t=>{if(n.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var a=t&&("load"===t.type?"missing":t.type),l=t&&t.target&&t.target.src;d.message="Loading chunk "+r+" failed.\n("+a+": "+l+")",d.name="ChunkLoadError",d.type=a,d.request=l,o[1](d)}},"chunk-"+r,r)}else e[r]=0},n.O.j=r=>0===e[r];var r=(r,t)=>{var o,a,[l,d,i]=t,s=0;for(o in d)n.o(d,o)&&(n.m[o]=d[o]);if(i)var u=i(n);for(r&&r(t);s<l.length;s++)n.o(e,a=l[s])&&e[a]&&e[a][0](),e[l[s]]=0;return n.O(u)},t=self.webpackChunkrtl=self.webpackChunkrtl||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})()})();

File diff suppressed because one or more lines are too long

@ -4,46 +4,6 @@ import { Common } from '../../utils/common.js';
let options = null;
const logger = Logger;
const common = Common;
export const arrangeChannels = (simplifiedChannels) => {
let channelTotal = 0;
let totalLocalBalance = 0;
let totalRemoteBalance = 0;
let lightningBalances = { localBalance: 0, remoteBalance: 0 };
const channelStatus = { active: { channels: 0, capacity: 0 }, inactive: { channels: 0, capacity: 0 }, pending: { channels: 0, capacity: 0 } };
let activeChannels = [];
const pendingChannels = [];
const inactiveChannels = [];
simplifiedChannels.forEach((channel, i) => {
if (channel.state === 'NORMAL') {
channelTotal = channel.toLocal + channel.toRemote;
totalLocalBalance = totalLocalBalance + channel.toLocal;
totalRemoteBalance = totalRemoteBalance + channel.toRemote;
channel.balancedness = (channelTotal === 0) ? 1 : (1 - Math.abs((channel.toLocal - channel.toRemote) / channelTotal)).toFixed(3);
activeChannels.push(channel);
channelStatus.active.channels = channelStatus.active.channels + 1;
channelStatus.active.capacity = channelStatus.active.capacity + channel.toLocal;
}
else if (channel.state.includes('WAIT') || channel.state.includes('CLOSING') || channel.state.includes('SYNCING')) {
channel.state = channel.state.replace(/_/g, ' ');
pendingChannels.push(channel);
channelStatus.pending.channels = channelStatus.pending.channels + 1;
channelStatus.pending.capacity = channelStatus.pending.capacity + channel.toLocal;
}
else {
channel.state = channel.state.replace(/_/g, ' ');
inactiveChannels.push(channel);
channelStatus.inactive.channels = channelStatus.inactive.channels + 1;
channelStatus.inactive.capacity = channelStatus.inactive.capacity + channel.toLocal;
}
});
lightningBalances = { localBalance: totalLocalBalance, remoteBalance: totalRemoteBalance };
activeChannels = common.sortDescByKey(activeChannels, 'balancedness');
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Lightning Balances', data: lightningBalances });
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Active Channels', data: activeChannels });
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Pending Channels', data: pendingChannels });
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Inactive Channels', data: inactiveChannels });
return ({ activeChannels: activeChannels, pendingChannels: pendingChannels, inactiveChannels: inactiveChannels, lightningBalances: lightningBalances, channelStatus: channelStatus });
};
export const simplifyAllChannels = (channels) => {
let channelNodeIds = '';
const simplifiedChannels = [];
@ -91,7 +51,7 @@ export const getChannels = (req, res, next) => {
}
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: options });
if (common.read_dummy_data) {
common.getDummyData('Channels').then((data) => { res.status(200).json(arrangeChannels(data)); });
common.getDummyData('Channels').then((data) => { res.status(200).json(data); });
}
else {
request.post(options).then((body) => {
@ -100,7 +60,7 @@ export const getChannels = (req, res, next) => {
return simplifyAllChannels(body).then((simplifiedChannels) => {
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Simplified Channels with Alias', data: simplifiedChannels });
logger.log({ level: 'INFO', fileName: 'Channels', msg: 'Channels List Received' });
res.status(200).json(arrangeChannels(simplifiedChannels));
res.status(200).json(simplifiedChannels);
});
}
else {

@ -1,9 +1,11 @@
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { ECLWSClient } from './webSocketClient.js';
let options = null;
const logger = Logger;
const common = Common;
const eclWsClient = ECLWSClient;
export const getInfo = (req, res, next) => {
logger.log({ level: 'INFO', fileName: 'GetInfo', msg: 'Getting Eclair Node Information..' });
common.setOptions();
@ -26,6 +28,8 @@ export const getInfo = (req, res, next) => {
}
else {
request.post(options).then((body) => {
logger.log({ level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Eclair\'s Websocket Server.' });
eclWsClient.connect();
logger.log({ level: 'DEBUG', fileName: 'GetInfo', msg: 'Get Info Response', data: body });
body.lnImplementation = 'Eclair';
logger.log({ level: 'INFO', fileName: 'GetInfo', msg: 'Eclair Node Information Received' });

@ -0,0 +1,66 @@
import WebSocket from 'ws';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { WSServer } from '../../utils/webSocketServer.js';
export class ECLWebSocketClient {
constructor() {
this.logger = Logger;
this.common = Common;
this.wsServer = WSServer;
this.webSocketClient = null;
this.reconnectTimeOut = null;
this.waitTime = 0.5;
this.reconnet = () => {
if (this.reconnectTimeOut) {
return;
}
this.waitTime = (this.waitTime >= 16) ? 16 : (this.waitTime * 2);
this.reconnectTimeOut = setTimeout(() => {
this.logger.log({ level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Reconnecting to the Eclair\'s Websocket Server.' });
this.connect();
this.reconnectTimeOut = null;
}, this.waitTime * 1000);
};
this.connect = () => {
try {
const UpdatedLNServerURL = this.common.getSelLNServerUrl().replace(/^http/, 'ws');
const firstSubStrIndex = (UpdatedLNServerURL.indexOf('//') + 2);
const WS_LINK = UpdatedLNServerURL.slice(0, firstSubStrIndex) + ':' + this.common.selectedNode.ln_api_password + '@' + UpdatedLNServerURL.slice(firstSubStrIndex) + '/ws';
this.webSocketClient = new WebSocket(WS_LINK);
this.webSocketClient.onopen = this.onClientOpen;
this.webSocketClient.onclose = this.onClientClose;
this.webSocketClient.onmessage = this.onClientMessage;
this.webSocketClient.onerror = this.onClientError;
}
catch (err) {
throw new Error(err);
}
};
this.onClientOpen = () => {
this.logger.log({ level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Connected to the Eclair\'s Websocket Server.' });
this.waitTime = 0.5;
};
this.onClientClose = (e) => {
if (this.common.selectedNode.ln_implementation === 'ECL') {
this.logger.log({ level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Web socket disconnected, will reconnect again..' });
this.reconnet();
}
};
this.onClientMessage = (msg) => {
this.logger.log({ level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Received message from the server..', data: msg.data });
this.wsServer.sendEventsToAllWSClients(msg.data);
};
this.onClientError = (err) => {
this.logger.log({ level: 'ERROR', fileName: 'ECLWebSocket', msg: 'Web socket error', error: err });
this.wsServer.sendErrorToAllWSClients(err);
this.reconnet();
};
this.disconnect = () => {
if (this.webSocketClient && this.webSocketClient.readyState === 1) {
this.logger.log({ level: 'INFO', fileName: 'ECLWebSocket', msg: 'Disconnecting from the Eclair\'s Websocket Server.' });
this.webSocketClient.close();
}
};
}
}
export const ECLWSClient = new ECLWebSocketClient();

@ -4,11 +4,26 @@ import parseHocon from 'hocon-parser';
import request from 'request-promise';
import { Logger } from '../../utils/logger.js';
import { Common } from '../../utils/common.js';
import { ECLWSClient } from '../eclair/webSocketClient.js';
const options = { url: '' };
const logger = Logger;
const common = Common;
const eclWsClient = ECLWSClient;
export const updateSelectedNode = (req, res, next) => {
logger.log({ level: 'INFO', fileName: 'RTLConf', msg: 'Updating Selected Node..' });
switch (common.selectedNode.ln_implementation) {
case 'LND':
// lndWsClient.disconnect();
break;
case 'CLT':
// clWsClient.disconnect();
break;
case 'ECL':
eclWsClient.disconnect();
break;
default:
break;
}
const selNodeIndex = req.body.selNodeIndex;
common.selectedNode = common.findNode(selNodeIndex);
const responseVal = common.selectedNode && common.selectedNode.ln_node ? common.selectedNode.ln_node : '';

@ -0,0 +1,6 @@
import { Router } from 'express';
import { isAuthenticated } from '../../utils/authCheck';
import { SSEventControl } from '../../controllers/eclair/webSocket';
const router = Router();
router.get('/', isAuthenticated, SSEventControl);
export default router;

@ -20,7 +20,6 @@ export class ExpressApplication {
this.logger = Logger;
this.common = Common;
this.config = Config;
this.baseHref = '/rtl';
this.directoryName = dirname(fileURLToPath(import.meta.url));
this.getApp = () => this.app;
this.loadConfiguration = () => {
@ -30,22 +29,22 @@ export class ExpressApplication {
this.logger.log({ level: 'INFO', fileName: 'App', msg: 'LOAD DATABASE: IN PROGRESS' });
const adapter = new JSONFile(join(this.directoryName, '../..', 'db', 'db.json'));
const db = new Low(adapter);
yield db.read();
db.data.posts.push('Hello World');
this.logger.log({ level: 'INFO', fileName: 'App', msg: 'Test Data:', data: db.data.posts });
db.data.posts.push('Next Post');
yield db.write();
this.logger.log({ level: 'INFO', fileName: 'App', msg: 'Test Data After Write:', data: db.data.posts });
// await db.read();
// db.data.posts.push('Hello World');
// this.logger.log({ level: 'INFO', fileName: 'App', msg: 'Test Data:', data: db.data.posts });
// db.data.posts.push('Next Post');
// await db.write();
// this.logger.log({ level: 'INFO', fileName: 'App', msg: 'Test Data After Write:', data: db.data.posts });
});
this.setCORS = () => { CORS.mount(this.app); };
this.setCSRF = () => { CSRF.mount(this.app); };
this.setApplicationRoutes = () => {
this.logger.log({ level: 'DEBUG', fileName: 'App', msg: 'Setting up Application Routes.' });
this.app.use(this.baseHref + '/api', sharedRoutes);
this.app.use(this.baseHref + '/api/lnd', lndRoutes);
this.app.use(this.baseHref + '/api/cl', clRoutes);
this.app.use(this.baseHref + '/api/ecl', eclRoutes);
this.app.use(this.baseHref, express.static(join(this.directoryName, '../..', 'angular')));
this.app.use(this.common.baseHref + '/api', sharedRoutes);
this.app.use(this.common.baseHref + '/api/lnd', lndRoutes);
this.app.use(this.common.baseHref + '/api/cl', clRoutes);
this.app.use(this.common.baseHref + '/api/ecl', eclRoutes);
this.app.use(this.common.baseHref, express.static(join(this.directoryName, '../..', 'angular')));
this.app.use((req, res, next) => {
res.cookie('XSRF-TOKEN', req.csrfToken ? req.csrfToken() : '');
res.sendFile(join(this.directoryName, '../..', 'angular', 'index.html'));

@ -1,7 +1,9 @@
import jwt from 'jsonwebtoken';
import csurf from 'csurf/index.js';
import { Common } from './common.js';
const common = Common;
const csurfProtection = csurf({ cookie: true });
export const isAuthenticated = (req, res, next) => {
const common = Common;
try {
const token = req.headers.authorization.split(' ')[1];
jwt.verify(token, common.secret_key);
@ -13,3 +15,30 @@ export const isAuthenticated = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
};
export const verifyWSUser = (info, next) => {
const protocols = !info.req.headers['sec-websocket-protocol'] ? [] : info.req.headers['sec-websocket-protocol'].split(',').map((s) => s.trim());
const jwToken = (protocols && protocols.length > 0) ? protocols[0] : '';
if (!jwToken || jwToken === '') {
next(false, 401, 'Authentication Failed! Please Login First!');
}
else {
jwt.verify(jwToken, common.secret_key, (verificationErr) => {
if (verificationErr) {
next(false, 401, 'Authentication Failed! Please Login First!');
}
else {
const updatedReq = JSON.parse(JSON.stringify(info.req));
updatedReq['cookies'] = !updatedReq.headers.cookie ? {} : '{"' + updatedReq.headers.cookie.replace(/ /g, '').replace(/;/g, '","').trim().replace(/[=]/g, '":"') + '"}';
updatedReq['cookies'] = JSON.parse(updatedReq['cookies']);
csurfProtection(updatedReq, null, (err) => {
if (err) {
next(false, 403, 'Invalid CSRF token!');
}
else {
next(true);
}
});
}
});
}
};

@ -22,6 +22,7 @@ export class CommonService {
this.cookie = '';
this.read_dummy_data = false;
this.platform = '/';
this.baseHref = '/rtl';
this.dummy_data_array_from_file = [];
this.MONTHS = [{ name: 'JAN', days: 31 }, { name: 'FEB', days: 28 }, { name: 'MAR', days: 31 }, { name: 'APR', days: 30 }, { name: 'MAY', days: 31 }, { name: 'JUN', days: 30 }, { name: 'JUL', days: 31 }, { name: 'AUG', days: 31 }, { name: 'SEP', days: 30 }, { name: 'OCT', days: 31 }, { name: 'NOV', days: 30 }, { name: 'DEC', days: 31 }];
this.getSwapServerOptions = () => {

@ -9,7 +9,7 @@ class CORS {
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, filePath');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS');
if (process.env.NODE_ENV === 'development') {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin ? req.headers.origin : '');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin ? req.headers.origin : req.headers.host ? req.headers.host : '');
}
next();
});

@ -0,0 +1,75 @@
import * as crypto from 'crypto';
import WebSocket from 'ws';
import { Logger } from './logger.js';
import { Common } from './common.js';
import { verifyWSUser } from './authCheck.js';
export class WebSocketServer {
constructor() {
this.logger = Logger;
this.common = Common;
this.webSocketServer = null;
this.mount = (httpServer) => {
this.logger.log({ level: 'DEBUG', fileName: 'WebSocketServer', msg: 'Connecting Websocket Server.' });
this.webSocketServer = new WebSocket.Server({ noServer: true, path: this.common.baseHref + '/api/ws', verifyClient: (process.env.NODE_ENV === 'development') ? null : verifyWSUser });
httpServer.on('upgrade', (request, socket, head) => {
if (request.headers['upgrade'] !== 'websocket') {
socket.end('HTTP/1.1 400 Bad Request');
return;
}
const acceptKey = request.headers['sec-websocket-key'];
const hash = this.generateAcceptValue(acceptKey);
const responseHeaders = ['HTTP/1.1 101 Web Socket Protocol Handshake', 'Upgrade: WebSocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + hash];
const protocols = !request.headers['sec-websocket-protocol'] ? [] : request.headers['sec-websocket-protocol'].split(',').map((s) => s.trim());
if (protocols.includes('json')) {
responseHeaders.push('Sec-WebSocket-Protocol: json');
}
this.webSocketServer.handleUpgrade(request, socket, head, this.upgradeCallback);
});
this.webSocketServer.on('connection', this.mountEventsOnConnection);
};
this.upgradeCallback = (websocket, request) => {
this.webSocketServer.emit('connection', websocket, request);
};
this.mountEventsOnConnection = (websocket, request) => {
websocket.clientId = Date.now();
this.logger.log({ level: 'INFO', fileName: 'WebSocketServer', msg: 'Connected: ' + websocket.clientId + ', Total WS clients: ' + this.webSocketServer.clients.size });
websocket.on('error', this.sendErrorToAllWSClients);
websocket.on('message', this.sendEventsToAllWSClients);
websocket.on('close', () => { this.logger.log({ level: 'INFO', fileName: 'WebSocketServer', msg: 'Disconnected: ' + websocket.clientId + ', Total WS clients: ' + this.webSocketServer.clients.size }); });
};
this.sendErrorToClient = (client, serverError) => {
try {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Sending error to client...: ' + JSON.stringify(serverError) });
client.send(JSON.stringify({ error: serverError }));
client.close();
}
catch (err) {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Error while sending error: ' + JSON.stringify(err) });
}
};
this.sendErrorToAllWSClients = (serverError) => {
try {
this.webSocketServer.clients.forEach((client) => {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Broadcasting error to clients...: ' + JSON.stringify(serverError) });
client.send(JSON.stringify({ error: serverError }));
});
}
catch (err) {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Error while broadcasting message: ' + JSON.stringify(err) });
}
};
this.sendEventsToAllWSClients = (newMessage) => {
try {
this.webSocketServer.clients.forEach((client) => {
this.logger.log({ level: 'INFO', fileName: 'WebSocketServer', msg: 'Broadcasting message to client...: ' + client.clientId });
client.send(newMessage);
});
}
catch (err) {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Error while broadcasting message: ' + JSON.stringify(err) });
}
};
this.generateAcceptValue = (acceptKey) => crypto.createHash('sha1').update(acceptKey + crypto.randomBytes(64).toString('hex')).digest('base64');
}
}
export const WSServer = new WebSocketServer();

@ -7,6 +7,7 @@
"ng": "ng",
"start": "ng serve --open",
"prebuild": "node ./src/prebuild",
"buildsource": "ng build --configuration production && npm run buildserver",
"build": "ng analytics off && ng lint && ng test && ng build --configuration production && npm run buildserver",
"buildserver": "tsc --project tsconfig.json",
"watchserver": "tsc --project tsconfig.json --watch",

@ -2,9 +2,11 @@ import http from 'http';
import App from './backend/utils/app.js';
import { Logger } from './backend/utils/logger.js';
import { Common } from './backend/utils/common.js';
import { WSServer } from './backend/utils/webSocketServer.js';
const logger = Logger;
const common = Common;
const wsServer = WSServer;
const app = new App();
const onError = (error) => {
@ -36,11 +38,13 @@ const onListening = () => {
logger.log({ level: 'INFO', fileName: 'RTL', msg: 'Server is up and running, please open the UI at http://' + (common.host ? common.host : 'localhost') + ':' + common.port });
};
const server = http.createServer(app.getApp());
let server = http.createServer(app.getApp());
server.on('error', onError);
server.on('listening', onListening);
wsServer.mount(server);
if (common.host) {
server.listen(common.port, common.host);
} else {

@ -5,45 +5,6 @@ let options = null;
const logger: LoggerService = Logger;
const common: CommonService = Common;
export const arrangeChannels = (simplifiedChannels) => {
let channelTotal = 0;
let totalLocalBalance = 0;
let totalRemoteBalance = 0;
let lightningBalances = { localBalance: 0, remoteBalance: 0 };
const channelStatus = { active: { channels: 0, capacity: 0 }, inactive: { channels: 0, capacity: 0 }, pending: { channels: 0, capacity: 0 } };
let activeChannels = [];
const pendingChannels = [];
const inactiveChannels = [];
simplifiedChannels.forEach((channel, i) => {
if (channel.state === 'NORMAL') {
channelTotal = channel.toLocal + channel.toRemote;
totalLocalBalance = totalLocalBalance + channel.toLocal;
totalRemoteBalance = totalRemoteBalance + channel.toRemote;
channel.balancedness = (channelTotal === 0) ? 1 : (1 - Math.abs((channel.toLocal - channel.toRemote) / channelTotal)).toFixed(3);
activeChannels.push(channel);
channelStatus.active.channels = channelStatus.active.channels + 1;
channelStatus.active.capacity = channelStatus.active.capacity + channel.toLocal;
} else if (channel.state.includes('WAIT') || channel.state.includes('CLOSING') || channel.state.includes('SYNCING')) {
channel.state = channel.state.replace(/_/g, ' ');
pendingChannels.push(channel);
channelStatus.pending.channels = channelStatus.pending.channels + 1;
channelStatus.pending.capacity = channelStatus.pending.capacity + channel.toLocal;
} else {
channel.state = channel.state.replace(/_/g, ' ');
inactiveChannels.push(channel);
channelStatus.inactive.channels = channelStatus.inactive.channels + 1;
channelStatus.inactive.capacity = channelStatus.inactive.capacity + channel.toLocal;
}
});
lightningBalances = { localBalance: totalLocalBalance, remoteBalance: totalRemoteBalance };
activeChannels = common.sortDescByKey(activeChannels, 'balancedness');
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Lightning Balances', data: lightningBalances });
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Active Channels', data: activeChannels });
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Pending Channels', data: pendingChannels });
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Inactive Channels', data: inactiveChannels });
return ({ activeChannels: activeChannels, pendingChannels: pendingChannels, inactiveChannels: inactiveChannels, lightningBalances: lightningBalances, channelStatus: channelStatus });
};
export const simplifyAllChannels = (channels) => {
let channelNodeIds = '';
const simplifiedChannels = [];
@ -92,7 +53,7 @@ export const getChannels = (req, res, next) => {
}
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Options', data: options });
if (common.read_dummy_data) {
common.getDummyData('Channels').then((data) => { res.status(200).json(arrangeChannels(data)); });
common.getDummyData('Channels').then((data) => { res.status(200).json(data); });
} else {
request.post(options).then((body) => {
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'All Channels', data: body });
@ -100,7 +61,7 @@ export const getChannels = (req, res, next) => {
return simplifyAllChannels(body).then((simplifiedChannels) => {
logger.log({ level: 'DEBUG', fileName: 'Channels', msg: 'Simplified Channels with Alias', data: simplifiedChannels });
logger.log({ level: 'INFO', fileName: 'Channels', msg: 'Channels List Received' });
res.status(200).json(arrangeChannels(simplifiedChannels));
res.status(200).json(simplifiedChannels);
});
} else {
logger.log({ level: 'INFO', fileName: 'Channels', msg: 'Empty Channels List Received' });

@ -1,9 +1,12 @@
import request from 'request-promise';
import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js';
import { ECLWSClient, ECLWebSocketClient } from './webSocketClient.js';
let options = null;
const logger: LoggerService = Logger;
const common: CommonService = Common;
const eclWsClient: ECLWebSocketClient = ECLWSClient;
export const getInfo = (req, res, next) => {
logger.log({ level: 'INFO', fileName: 'GetInfo', msg: 'Getting Eclair Node Information..' });
@ -25,6 +28,8 @@ export const getInfo = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error });
} else {
request.post(options).then((body) => {
logger.log({ level: 'INFO', fileName: 'GetInfo', msg: 'Connecting to the Eclair\'s Websocket Server.' });
eclWsClient.connect();
logger.log({ level: 'DEBUG', fileName: 'GetInfo', msg: 'Get Info Response', data: body });
body.lnImplementation = 'Eclair';
logger.log({ level: 'INFO', fileName: 'GetInfo', msg: 'Eclair Node Information Received' });

@ -0,0 +1,73 @@
import WebSocket from 'ws';
import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js';
import { WSServer } from '../../utils/webSocketServer.js';
export class ECLWebSocketClient {
public logger: LoggerService = Logger;
public common: CommonService = Common;
public wsServer = WSServer;
public webSocketClient = null;
public reconnectTimeOut = null;
public waitTime = 0.5;
public reconnet = () => {
if (this.reconnectTimeOut) { return; }
this.waitTime = (this.waitTime >= 16) ? 16 : (this.waitTime * 2);
this.reconnectTimeOut = setTimeout(() => {
this.logger.log({ level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Reconnecting to the Eclair\'s Websocket Server.' });
this.connect();
this.reconnectTimeOut = null;
}, this.waitTime * 1000);
};
public connect = () => {
try {
const UpdatedLNServerURL = this.common.getSelLNServerUrl().replace(/^http/, 'ws');
const firstSubStrIndex = (UpdatedLNServerURL.indexOf('//') + 2);
const WS_LINK = UpdatedLNServerURL.slice(0, firstSubStrIndex) + ':' + this.common.selectedNode.ln_api_password + '@' + UpdatedLNServerURL.slice(firstSubStrIndex) + '/ws';
this.webSocketClient = new WebSocket(WS_LINK);
this.webSocketClient.onopen = this.onClientOpen;
this.webSocketClient.onclose = this.onClientClose;
this.webSocketClient.onmessage = this.onClientMessage;
this.webSocketClient.onerror = this.onClientError;
} catch (err) {
throw new Error(err);
}
};
public onClientOpen = () => {
this.logger.log({ level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Connected to the Eclair\'s Websocket Server.' });
this.waitTime = 0.5;
};
public onClientClose = (e) => {
if (this.common.selectedNode.ln_implementation === 'ECL') {
this.logger.log({ level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Web socket disconnected, will reconnect again..' });
this.reconnet();
}
};
public onClientMessage = (msg) => {
this.logger.log({ level: 'DEBUG', fileName: 'ECLWebSocket', msg: 'Received message from the server..', data: msg.data });
this.wsServer.sendEventsToAllWSClients(msg.data);
};
public onClientError = (err) => {
this.logger.log({ level: 'ERROR', fileName: 'ECLWebSocket', msg: 'Web socket error', error: err });
this.wsServer.sendErrorToAllWSClients(err);
this.reconnet();
};
public disconnect = () => {
if (this.webSocketClient && this.webSocketClient.readyState === 1) {
this.logger.log({ level: 'INFO', fileName: 'ECLWebSocket', msg: 'Disconnecting from the Eclair\'s Websocket Server.' });
this.webSocketClient.close();
}
};
}
export const ECLWSClient = new ECLWebSocketClient();

@ -5,16 +5,35 @@ import request from 'request-promise';
import { Logger, LoggerService } from '../../utils/logger.js';
import { Common, CommonService } from '../../utils/common.js';
import { AuthenticationConfiguration, NodeSettingsConfiguration } from '../../models/config.model.js';
import { ECLWSClient, ECLWebSocketClient } from '../eclair/webSocketClient.js';
const options = { url: '' };
const logger: LoggerService = Logger;
const common: CommonService = Common;
const eclWsClient: ECLWebSocketClient = ECLWSClient;
export const updateSelectedNode = (req, res, next) => {
logger.log({ level: 'INFO', fileName: 'RTLConf', msg: 'Updating Selected Node..' });
switch (common.selectedNode.ln_implementation) {
case 'LND':
// lndWsClient.disconnect();
break;
case 'CLT':
// clWsClient.disconnect();
break;
case 'ECL':
eclWsClient.disconnect();
break;
default:
break;
}
const selNodeIndex = req.body.selNodeIndex;
common.selectedNode = common.findNode(selNodeIndex);
const responseVal = common.selectedNode && common.selectedNode.ln_node ? common.selectedNode.ln_node : '';
logger.log({ level: 'DEBUG', fileName: 'RTLConf', msg: 'Selected Node Updated To', data: responseVal });
logger.log({ level: 'INFO', fileName: 'RTLConf', msg: 'Selected Node Updated' });
res.status(200).json({ status: 'Selected Node Updated To: ' + JSON.stringify(responseVal) + '!' });

@ -24,7 +24,6 @@ export class ExpressApplication {
public logger: LoggerService = Logger;
public common: CommonService = Common;
public config: ConfigService = Config;
public baseHref = '/rtl';
public directoryName = dirname(fileURLToPath(import.meta.url));
constructor() {
@ -52,13 +51,12 @@ export class ExpressApplication {
this.logger.log({ level: 'INFO', fileName: 'App', msg: 'LOAD DATABASE: IN PROGRESS' });
const adapter = new JSONFile<DBDataType>(join(this.directoryName, '../..', 'db', 'db.json'));
const db = new Low<DBDataType>(adapter);
await db.read();
db.data.posts.push('Hello World');
this.logger.log({ level: 'INFO', fileName: 'App', msg: 'Test Data:', data: db.data.posts });
db.data.posts.push('Next Post');
await db.write();
this.logger.log({ level: 'INFO', fileName: 'App', msg: 'Test Data After Write:', data: db.data.posts });
// await db.read();
// db.data.posts.push('Hello World');
// this.logger.log({ level: 'INFO', fileName: 'App', msg: 'Test Data:', data: db.data.posts });
// db.data.posts.push('Next Post');
// await db.write();
// this.logger.log({ level: 'INFO', fileName: 'App', msg: 'Test Data After Write:', data: db.data.posts });
}
public setCORS = () => { CORS.mount(this.app); }
@ -68,11 +66,11 @@ export class ExpressApplication {
public setApplicationRoutes = () => {
this.logger.log({ level: 'DEBUG', fileName: 'App', msg: 'Setting up Application Routes.' });
this.app.use(this.baseHref + '/api', sharedRoutes);
this.app.use(this.baseHref + '/api/lnd', lndRoutes);
this.app.use(this.baseHref + '/api/cl', clRoutes);
this.app.use(this.baseHref + '/api/ecl', eclRoutes);
this.app.use(this.baseHref, express.static(join(this.directoryName, '../..', 'angular')));
this.app.use(this.common.baseHref + '/api', sharedRoutes);
this.app.use(this.common.baseHref + '/api/lnd', lndRoutes);
this.app.use(this.common.baseHref + '/api/cl', clRoutes);
this.app.use(this.common.baseHref + '/api/ecl', eclRoutes);
this.app.use(this.common.baseHref, express.static(join(this.directoryName, '../..', 'angular')));
this.app.use((req, res, next) => {
res.cookie('XSRF-TOKEN', req.csrfToken ? req.csrfToken() : '');
res.sendFile(join(this.directoryName, '../..', 'angular', 'index.html'));

@ -1,8 +1,11 @@
import jwt from 'jsonwebtoken';
import csurf from 'csurf/index.js';
import { Common, CommonService } from './common.js';
const common: CommonService = Common;
const csurfProtection = csurf({ cookie: true });
export const isAuthenticated = (req, res, next) => {
const common: CommonService = Common;
try {
const token = req.headers.authorization.split(' ')[1];
jwt.verify(token, common.secret_key);
@ -13,3 +16,28 @@ export const isAuthenticated = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
};
export const verifyWSUser = (info, next) => {
const protocols = !info.req.headers['sec-websocket-protocol'] ? [] : info.req.headers['sec-websocket-protocol'].split(',').map((s) => s.trim());
const jwToken = (protocols && protocols.length > 0) ? protocols[0] : '';
if (!jwToken || jwToken === '') {
next(false, 401, 'Authentication Failed! Please Login First!');
} else {
jwt.verify(jwToken, common.secret_key, (verificationErr) => {
if (verificationErr) {
next(false, 401, 'Authentication Failed! Please Login First!');
} else {
const updatedReq = JSON.parse(JSON.stringify(info.req));
updatedReq['cookies'] = !updatedReq.headers.cookie ? {} : '{"' + updatedReq.headers.cookie.replace(/ /g, '').replace(/;/g, '","').trim().replace(/[=]/g, '":"') + '"}';
updatedReq['cookies'] = JSON.parse(updatedReq['cookies']);
csurfProtection(updatedReq, null, (err) => {
if (err) {
next(false, 403, 'Invalid CSRF token!');
} else {
next(true);
}
});
}
});
}
};

@ -24,6 +24,7 @@ export class CommonService {
public cookie = '';
public read_dummy_data = false;
public platform = '/';
public baseHref = '/rtl';
private dummy_data_array_from_file = [];
private MONTHS = [{ name: 'JAN', days: 31 }, { name: 'FEB', days: 28 }, { name: 'MAR', days: 31 }, { name: 'APR', days: 30 }, { name: 'MAY', days: 31 }, { name: 'JUN', days: 30 }, { name: 'JUL', days: 31 }, { name: 'AUG', days: 31 }, { name: 'SEP', days: 30 }, { name: 'OCT', days: 31 }, { name: 'NOV', days: 30 }, { name: 'DEC', days: 31 }];

@ -11,7 +11,7 @@ class CORS {
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, filePath');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS');
if (process.env.NODE_ENV === 'development') {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin ? req.headers.origin : '');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin ? req.headers.origin : req.headers.host ? req.headers.host : '');
}
next();
});

@ -0,0 +1,81 @@
import * as crypto from 'crypto';
import WebSocket from 'ws';
import { Application } from 'express';
import { Logger, LoggerService } from './logger.js';
import { Common, CommonService } from './common.js';
import { verifyWSUser } from './authCheck.js';
export class WebSocketServer {
public logger: LoggerService = Logger;
public common: CommonService = Common;
public webSocketServer = null;
public mount = (httpServer: Application): Application => {
this.logger.log({ level: 'DEBUG', fileName: 'WebSocketServer', msg: 'Connecting Websocket Server.' });
this.webSocketServer = new WebSocket.Server({ noServer: true, path: this.common.baseHref + '/api/ws', verifyClient: (process.env.NODE_ENV === 'development') ? null : verifyWSUser });
httpServer.on('upgrade', (request, socket, head) => {
if (request.headers['upgrade'] !== 'websocket') {
socket.end('HTTP/1.1 400 Bad Request');
return;
}
const acceptKey = request.headers['sec-websocket-key'];
const hash = this.generateAcceptValue(acceptKey);
const responseHeaders = ['HTTP/1.1 101 Web Socket Protocol Handshake', 'Upgrade: WebSocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + hash];
const protocols = !request.headers['sec-websocket-protocol'] ? [] : request.headers['sec-websocket-protocol'].split(',').map((s) => s.trim());
if (protocols.includes('json')) { responseHeaders.push('Sec-WebSocket-Protocol: json'); }
this.webSocketServer.handleUpgrade(request, socket, head, this.upgradeCallback);
});
this.webSocketServer.on('connection', this.mountEventsOnConnection);
}
public upgradeCallback = (websocket, request) => {
this.webSocketServer.emit('connection', websocket, request);
};
public mountEventsOnConnection = (websocket, request) => {
websocket.clientId = Date.now();
this.logger.log({ level: 'INFO', fileName: 'WebSocketServer', msg: 'Connected: ' + websocket.clientId + ', Total WS clients: ' + this.webSocketServer.clients.size });
websocket.on('error', this.sendErrorToAllWSClients);
websocket.on('message', this.sendEventsToAllWSClients);
websocket.on('close', () => { this.logger.log({ level: 'INFO', fileName: 'WebSocketServer', msg: 'Disconnected: ' + websocket.clientId + ', Total WS clients: ' + this.webSocketServer.clients.size }); });
};
public sendErrorToClient = (client, serverError) => {
try {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Sending error to client...: ' + JSON.stringify(serverError) });
client.send(JSON.stringify({ error: serverError }));
client.close();
} catch (err) {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Error while sending error: ' + JSON.stringify(err) });
}
};
public sendErrorToAllWSClients = (serverError) => {
try {
this.webSocketServer.clients.forEach((client) => {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Broadcasting error to clients...: ' + JSON.stringify(serverError) });
client.send(JSON.stringify({ error: serverError }));
});
} catch (err) {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Error while broadcasting message: ' + JSON.stringify(err) });
}
};
public sendEventsToAllWSClients = (newMessage) => {
try {
this.webSocketServer.clients.forEach((client) => {
this.logger.log({ level: 'INFO', fileName: 'WebSocketServer', msg: 'Broadcasting message to client...: ' + client.clientId });
client.send(newMessage);
});
} catch (err) {
this.logger.log({ level: 'ERROR', fileName: 'WebSocketServer', msg: 'Error while broadcasting message: ' + JSON.stringify(err) });
}
};
public generateAcceptValue = (acceptKey) => crypto.createHash('sha1').update(acceptKey + crypto.randomBytes(64).toString('hex')).digest('base64');
}
export const WSServer = new WebSocketServer();

@ -18,6 +18,7 @@ import { AuthInterceptor } from './shared/services/auth.interceptor';
import { SessionService } from './shared/services/session.service';
import { LoopService } from './shared/services/loop.service';
import { DataService } from './shared/services/data.service';
import { WebSocketClientService } from './shared/services/web-socket.service';
import { CommonService } from './shared/services/common.service';
import { BoltzService } from './shared/services/boltz.service';
@ -48,7 +49,7 @@ import { LayoutModule } from '@angular/cdk/layout';
declarations: [AppComponent],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
AuthGuard, SessionService, DataService, LoopService, CommonService, BoltzService
AuthGuard, SessionService, DataService, WebSocketClientService, LoopService, CommonService, BoltzService
],
bootstrap: [AppComponent]
})

@ -60,6 +60,7 @@ export class CLEffects implements OnDestroy {
withLatestFrom(this.store.select('root')),
mergeMap(([action, store]: [CLActions.FetchInfo, fromRTLReducer.RootState]) => {
this.flgInitialized = false;
this.store.dispatch(new RTLActions.SetApiUrl(this.CHILD_API_URL));
this.store.dispatch(new CLActions.UpdateAPICallStatus({ action: 'FetchInfo', status: APICallStatusEnum.INITIATED }));
this.store.dispatch(new RTLActions.OpenSpinner(UI_MESSAGES.GET_NODE_INFO));
return this.httpClient.get<GetInfo>(this.CHILD_API_URL + environment.GETINFO_API).
@ -247,7 +248,7 @@ export class CLEffects implements OnDestroy {
mergeMap(([action, clData]: [CLActions.SaveNewPeer, fromCLReducers.CLState]) => {
this.store.dispatch(new RTLActions.OpenSpinner(UI_MESSAGES.CONNECT_PEER));
this.store.dispatch(new CLActions.UpdateAPICallStatus({ action: 'SaveNewPeer', status: APICallStatusEnum.INITIATED }));
return this.httpClient.post(this.CHILD_API_URL + environment.PEERS_API, { id: action.payload.id }).
return this.httpClient.post<Peer[]>(this.CHILD_API_URL + environment.PEERS_API, { id: action.payload.id }).
pipe(
map((postRes: Peer[]) => {
this.logger.info(postRes);

@ -1,9 +1,8 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
import { faSmile, faFrown } from '@fortawesome/free-regular-svg-icons';
import { faAngleDoubleDown, faAngleDoubleUp, faChartPie, faBolt, faServer, faNetworkWired } from '@fortawesome/free-solid-svg-icons';
@ -15,7 +14,6 @@ import { ApiCallsListECL } from '../../shared/models/apiCallsPayload';
import { SelNodeChild } from '../../shared/models/RTLconfig';
import * as fromRTLReducer from '../../store/rtl.reducers';
import * as ECLActions from '../store/ecl.actions';
@Component({
selector: 'rtl-ecl-home',
@ -57,7 +55,7 @@ export class ECLHomeComponent implements OnInit, OnDestroy {
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private actions: Actions, private commonService: CommonService, private router: Router) {
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private commonService: CommonService, private router: Router) {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.operatorCards = [

@ -5,6 +5,7 @@ import { takeUntil, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { ECLOpenChannelComponent } from '../open-channel-modal/open-channel.component';
import { WebSocketClientService } from '../../../../shared/services/web-socket.service';
import { LoggerService } from '../../../../shared/services/logger.service';
import { GetInfo, Peer } from '../../../../shared/models/eclModels';
import { SelNodeChild } from '../../../../shared/models/RTLconfig';
@ -28,9 +29,9 @@ export class ECLChannelsTablesComponent implements OnInit, OnDestroy {
public totalBalance = 0;
public links = [{ link: 'open', name: 'Open' }, { link: 'pending', name: 'Pending' }, { link: 'inactive', name: 'Inactive' }];
public activeLink = 0;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private router: Router) {}
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private router: Router, private wsService: WebSocketClientService) {}
ngOnInit() {
this.activeLink = this.links.findIndex((link) => link.link === this.router.url.substring(this.router.url.lastIndexOf('/') + 1));

@ -2,7 +2,7 @@ import { Action } from '@ngrx/store';
import { ApiCallStatusPayload } from '../../shared/models/apiCallsPayload';
import { SelNodeChild } from '../../shared/models/RTLconfig';
import { GetInfo, Channel, ChannelStats, Fees, Peer, LightningBalance, OnChainBalance, ChannelsStatus, Payments, Route, Transaction, SendPaymentOnChain, Invoice } from '../../shared/models/eclModels';
import { GetInfo, Channel, Fees, Peer, LightningBalance, OnChainBalance, ChannelsStatus, Payments, Route, Transaction, SendPaymentOnChain, Invoice, PaymentReceived, ChannelStateUpdate } from '../../shared/models/eclModels';
export const RESET_ECL_STORE = 'RESET_ECL_STORE';
export const UPDATE_API_CALL_STATUS_ECL = 'UPDATE_API_CALL_STATUS_ECL';
@ -15,8 +15,6 @@ export const FETCH_CHANNELS_ECL = 'FETCH_CHANNELS_ECL';
export const SET_ACTIVE_CHANNELS_ECL = 'SET_ACTIVE_CHANNELS_ECL';
export const SET_PENDING_CHANNELS_ECL = 'SET_PENDING_CHANNELS_ECL';
export const SET_INACTIVE_CHANNELS_ECL = 'SET_INACTIVE_CHANNELS_ECL';
export const FETCH_CHANNEL_STATS_ECL = 'FETCH_CHANNEL_STATS_ECL';
export const SET_CHANNEL_STATS_ECL = 'SET_CHANNEL_STATS_ECL';
export const FETCH_ONCHAIN_BALANCE_ECL = 'FETCH_ONCHAIN_BALANCE_ECL';
export const SET_ONCHAIN_BALANCE_ECL = 'SET_ONCHAIN_BALANCE_ECL';
export const FETCH_LIGHTNING_BALANCE_ECL = 'FETCH_LIGHTNING_BALANCE_ECL';
@ -54,6 +52,7 @@ export const UPDATE_INVOICE_ECL = 'UPDATE_INVOICE_ECL';
export const PEER_LOOKUP_ECL = 'PEER_LOOKUP_ECL';
export const INVOICE_LOOKUP_ECL = 'INVOICE_LOOKUP_ECL';
export const SET_LOOKUP_ECL = 'SET_LOOKUP_ECL';
export const UPDATE_CHANNEL_STATE_ECL = 'UPDATE_CHANNEL_STATE_ECL';
export class UpdateAPICallStatus implements Action {
@ -131,19 +130,6 @@ export class SetInactiveChannels implements Action {
}
export class FetchChannelStats implements Action {
readonly type = FETCH_CHANNEL_STATS_ECL;
}
export class SetChannelStats implements Action {
readonly type = SET_CHANNEL_STATS_ECL;
constructor(public payload: ChannelStats[]) {}
}
export class FetchOnchainBalance implements Action {
readonly type = FETCH_ONCHAIN_BALANCE_ECL;
@ -358,7 +344,7 @@ export class AddInvoice implements Action {
export class UpdateInvoice implements Action {
readonly type = UPDATE_INVOICE_ECL;
constructor(public payload: Invoice) {}
constructor(public payload: Invoice | PaymentReceived) {}
}
@ -384,14 +370,20 @@ export class SetLookup implements Action {
}
export class UpdateChannelState implements Action {
readonly type = UPDATE_CHANNEL_STATE_ECL;
constructor(public payload: ChannelStateUpdate) {}
}
export type ECLActions = ResetECLStore | UpdateAPICallStatus | SetChildNodeSettings |
FetchInfo | SetInfo | FetchFees | SetFees |
FetchChannels | SetActiveChannels | SetPendingChannels | SetInactiveChannels |
FetchPeers | SetPeers | AddPeer | DisconnectPeer | SaveNewPeer | RemovePeer | NewlyAddedPeer |
SetChannelsStatus | FetchChannelStats | SetChannelStats |
FetchOnchainBalance | SetOnchainBalance | GetNewAddress | SetNewAddress |
SendOnchainFunds | SendOnchainFundsRes | FetchTransactions | SetTransactions |
SetLightningBalance | FetchPeers | SetPeers | PeerLookup | InvoiceLookup | SetLookup |
SaveNewChannel | UpdateChannels | CloseChannel | RemoveChannel |
SaveNewChannel | UpdateChannels | CloseChannel | RemoveChannel | SetChannelsStatus |
FetchPayments | SetPayments | SendPayment | SendPaymentStatus |
FetchInvoices | SetInvoices | CreateInvoice | AddInvoice | UpdateInvoice;
FetchInvoices | SetInvoices | CreateInvoice | AddInvoice | UpdateInvoice | UpdateChannelState;

@ -12,21 +12,25 @@ import { LoggerService } from '../../shared/services/logger.service';
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 { GetInfo, Channel, OnChainBalance, LightningBalance, ChannelsStatus, ChannelStats, Peer, Audit, Transaction, Invoice } from '../../shared/models/eclModels';
import { APICallStatusEnum, UI_MESSAGES } from '../../shared/services/consts-enums-functions';
import { GetInfo, OnChainBalance, Peer, Audit, Transaction, Invoice, Channel, ChannelStateUpdate } from '../../shared/models/eclModels';
import { APICallStatusEnum, UI_MESSAGES, WSEventTypeEnum } from '../../shared/services/consts-enums-functions';
import { ECLInvoiceInformationComponent } from '../transactions/invoice-information-modal/invoice-information.component';
import * as fromRTLReducer from '../../store/rtl.reducers';
import * as fromECLReducer from './ecl.reducers';
import * as ECLActions from './ecl.actions';
import * as RTLActions from '../../store/rtl.actions';
import { WebSocketClientService } from '../../shared/services/web-socket.service';
@Injectable()
export class ECLEffects implements OnDestroy {
CHILD_API_URL = API_URL + '/ecl';
private flgInitialized = false;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
private flgReceivedPaymentUpdateFromWS = false;
private latestPaymentRes = '';
private rawChannelsList: Channel[] = [];
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(
private actions: Actions,
@ -36,6 +40,7 @@ export class ECLEffects implements OnDestroy {
private commonService: CommonService,
private logger: LoggerService,
private router: Router,
private wsService: WebSocketClientService,
private location: Location
) {
this.store.select('ecl').
@ -52,12 +57,55 @@ export class ECLEffects implements OnDestroy {
this.flgInitialized = true;
}
});
this.wsService.wsMessages.pipe(
takeUntil(this.unSubs[1]),
withLatestFrom(this.store.select('ecl'))).subscribe(([newMessage, eclStore]) => {
let snackBarMsg = '';
if (newMessage) {
switch (newMessage.type) {
case WSEventTypeEnum.PAYMENT_SENT:
if (newMessage && newMessage.id && this.latestPaymentRes === newMessage.id) {
this.flgReceivedPaymentUpdateFromWS = true;
snackBarMsg = 'Payment Sent: ' + ((newMessage.paymentHash) ? ('with payment hash ' + newMessage.paymentHash) : JSON.stringify(newMessage));
this.handleSendPaymentStatus(snackBarMsg);
}
break;
case WSEventTypeEnum.PAYMENT_FAILED:
if (newMessage && newMessage.id && this.latestPaymentRes === newMessage.id) {
this.flgReceivedPaymentUpdateFromWS = true;
snackBarMsg = 'Payment Failed: ' + ((newMessage.failures && newMessage.failures.length && newMessage.failures.length > 0 && newMessage.failures[0].t) ? newMessage.failures[0].t : (newMessage.failures && newMessage.failures.length && newMessage.failures.length > 0 && newMessage.failures[0].e && newMessage.failures[0].e.failureMessage) ? newMessage.failures[0].e.failureMessage : JSON.stringify(newMessage));
this.handleSendPaymentStatus(snackBarMsg);
}
break;
case WSEventTypeEnum.PAYMENT_RECEIVED:
this.store.dispatch(new ECLActions.UpdateInvoice(newMessage));
break;
case WSEventTypeEnum.CHANNEL_STATE_CHANGED:
if ((<ChannelStateUpdate>newMessage).currentState === 'NORMAL' || (<ChannelStateUpdate>newMessage).currentState === 'CLOSED') {
this.rawChannelsList = this.rawChannelsList.map((channel) => {
if (channel.channelId === (<ChannelStateUpdate>newMessage).channelId && channel.nodeId === (<ChannelStateUpdate>newMessage).remoteNodeId) {
channel.state = (<ChannelStateUpdate>newMessage).currentState;
}
return channel;
});
this.setChannelsAndStatusAndBalances();
} else {
this.store.dispatch(new ECLActions.UpdateChannelState(newMessage));
}
break;
default:
this.logger.info('Received Event from WS: ' + JSON.stringify(newMessage));
break;
}
}
});
}
infoFetchECL = createEffect(() => this.actions.pipe(
ofType(ECLActions.FETCH_INFO_ECL),
mergeMap((action: ECLActions.FetchInfo) => {
this.flgInitialized = false;
this.store.dispatch(new RTLActions.SetApiUrl(this.CHILD_API_URL));
this.store.dispatch(new RTLActions.OpenSpinner(UI_MESSAGES.GET_NODE_INFO));
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'FetchInfo', status: APICallStatusEnum.INITIATED }));
return this.httpClient.get<GetInfo>(this.CHILD_API_URL + environment.GETINFO_API).
@ -132,22 +180,17 @@ export class ECLEffects implements OnDestroy {
ofType(ECLActions.FETCH_CHANNELS_ECL),
mergeMap((action: ECLActions.FetchChannels) => {
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'FetchChannels', status: APICallStatusEnum.INITIATED }));
return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API).
return this.httpClient.get<Channel[]>(this.CHILD_API_URL + environment.CHANNELS_API).
pipe(
map((channelsRes: { activeChannels: Channel[], pendingChannels: Channel[], inactiveChannels: Channel[], lightningBalances: LightningBalance, channelStatus: ChannelsStatus }) => {
map((channelsRes: Channel[]) => {
this.logger.info(channelsRes);
this.rawChannelsList = channelsRes;
this.setChannelsAndStatusAndBalances();
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'FetchChannels', status: APICallStatusEnum.COMPLETED }));
this.store.dispatch(new ECLActions.SetActiveChannels((channelsRes && channelsRes.activeChannels.length > 0) ? channelsRes.activeChannels : []));
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 && action.payload.fetchPayments) {
this.store.dispatch(new ECLActions.FetchPayments());
}
return {
type: ECLActions.SET_CHANNELS_STATUS_ECL,
payload: channelsRes.channelStatus
};
return { type: RTLActions.VOID };
}),
catchError((err: any) => {
this.handleErrorWithoutAlert('FetchChannels', UI_MESSAGES.NO_SPINNER, 'Fetching Channels Failed.', err);
@ -157,28 +200,6 @@ export class ECLEffects implements OnDestroy {
})
));
channelStatsFetch = createEffect(() => this.actions.pipe(
ofType(ECLActions.FETCH_CHANNEL_STATS_ECL),
mergeMap((action: ECLActions.FetchChannelStats) => {
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'FetchChannelStats', status: APICallStatusEnum.INITIATED }));
return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/stats').
pipe(
map((channelStats: ChannelStats[]) => {
this.logger.info(channelStats);
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'FetchChannelStats', status: APICallStatusEnum.COMPLETED }));
return {
type: ECLActions.SET_CHANNEL_STATS_ECL,
payload: (channelStats && channelStats.length > 0) ? channelStats : []
};
}),
catchError((err: any) => {
this.handleErrorWithoutAlert('FetchChannelStats', UI_MESSAGES.NO_SPINNER, 'Fetching Channel Stats Failed.', err);
return of({ type: RTLActions.VOID });
})
);
})
));
fetchOnchainBalance = createEffect(() => this.actions.pipe(
ofType(ECLActions.FETCH_ONCHAIN_BALANCE_ECL),
mergeMap((action: ECLActions.FetchOnchainBalance) => {
@ -203,7 +224,7 @@ export class ECLEffects implements OnDestroy {
ofType(ECLActions.FETCH_PEERS_ECL),
mergeMap((action: ECLActions.FetchPeers) => {
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'FetchPeers', status: APICallStatusEnum.INITIATED }));
return this.httpClient.get(this.CHILD_API_URL + environment.PEERS_API).
return this.httpClient.get<Peer[]>(this.CHILD_API_URL + environment.PEERS_API).
pipe(
map((peers: Peer[]) => {
this.logger.info(peers);
@ -259,7 +280,7 @@ export class ECLEffects implements OnDestroy {
mergeMap(([action, eclData]: [ECLActions.SaveNewPeer, fromECLReducer.ECLState]) => {
this.store.dispatch(new RTLActions.OpenSpinner(UI_MESSAGES.CONNECT_PEER));
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'SaveNewPeer', status: APICallStatusEnum.INITIATED }));
return this.httpClient.post(this.CHILD_API_URL + environment.PEERS_API + ((action.payload.id.includes('@') ? '?uri=' : '?nodeId=') + action.payload.id), {}).
return this.httpClient.post<Peer[]>(this.CHILD_API_URL + environment.PEERS_API + ((action.payload.id.includes('@') ? '?uri=' : '?nodeId=') + action.payload.id), {}).
pipe(
map((postRes: Peer[]) => {
this.logger.info(postRes);
@ -419,21 +440,20 @@ export class ECLEffects implements OnDestroy {
sendPayment = createEffect(() => this.actions.pipe(
ofType(ECLActions.SEND_PAYMENT_ECL),
withLatestFrom(this.store.select('root')),
mergeMap(([action, store]: [ECLActions.SendPayment, any]) => {
mergeMap((action: ECLActions.SendPayment) => {
this.flgReceivedPaymentUpdateFromWS = false;
this.latestPaymentRes = '';
this.store.dispatch(new RTLActions.OpenSpinner(UI_MESSAGES.SEND_PAYMENT));
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'SendPayment', status: APICallStatusEnum.INITIATED }));
return this.httpClient.post(this.CHILD_API_URL + environment.PAYMENTS_API, action.payload).
pipe(
map((sendRes: any) => {
this.logger.info(sendRes);
this.latestPaymentRes = sendRes;
setTimeout(() => {
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'SendPayment', status: APICallStatusEnum.COMPLETED }));
this.store.dispatch(new ECLActions.SendPaymentStatus(sendRes));
this.store.dispatch(new RTLActions.CloseSpinner(UI_MESSAGES.SEND_PAYMENT));
this.store.dispatch(new ECLActions.FetchChannels({ fetchPayments: true }));
this.store.dispatch(new ECLActions.FetchPayments());
this.store.dispatch(new RTLActions.OpenSnackBar('Payment Submitted!'));
if (!this.flgReceivedPaymentUpdateFromWS) {
this.handleSendPaymentStatus('Payment Submitted!');
}
}, 3000);
return { type: RTLActions.VOID };
}),
@ -506,7 +526,7 @@ export class ECLEffects implements OnDestroy {
this.logger.info(postRes);
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'CreateInvoice', status: APICallStatusEnum.COMPLETED }));
this.store.dispatch(new RTLActions.CloseSpinner(UI_MESSAGES.CREATE_INVOICE));
postRes.timestamp = new Date().getTime() / 1000;
postRes.timestamp = Math.round(new Date().getTime() / 1000);
postRes.expiresAt = Math.round(postRes.timestamp + action.payload.expireIn);
postRes.description = action.payload.description;
postRes.status = 'unpaid';
@ -612,6 +632,59 @@ export class ECLEffects implements OnDestroy {
{ dispatch: false }
);
setChannelsAndStatusAndBalances() {
let channelTotal = 0;
let totalLocalBalance = 0;
let totalRemoteBalance = 0;
let lightningBalances = { localBalance: 0, remoteBalance: 0 };
let activeChannels = [];
const pendingChannels = [];
const inactiveChannels = [];
const channelStatus = { active: { channels: 0, capacity: 0 }, inactive: { channels: 0, capacity: 0 }, pending: { channels: 0, capacity: 0 } };
this.rawChannelsList.forEach((channel, i) => {
if (channel.state === 'NORMAL') {
channelTotal = channel.toLocal + channel.toRemote;
totalLocalBalance = totalLocalBalance + channel.toLocal;
totalRemoteBalance = totalRemoteBalance + channel.toRemote;
channel.balancedness = (channelTotal === 0) ? 1 : +(1 - Math.abs((channel.toLocal - channel.toRemote) / channelTotal)).toFixed(3);
activeChannels.push(channel);
channelStatus.active.channels = channelStatus.active.channels + 1;
channelStatus.active.capacity = channelStatus.active.capacity + channel.toLocal;
} else if (channel.state.includes('WAIT') || channel.state.includes('CLOSING') || channel.state.includes('SYNCING')) {
channel.state = channel.state.replace(/_/g, ' ');
pendingChannels.push(channel);
channelStatus.pending.channels = channelStatus.pending.channels + 1;
channelStatus.pending.capacity = channelStatus.pending.capacity + channel.toLocal;
} else {
channel.state = channel.state.replace(/_/g, ' ');
inactiveChannels.push(channel);
channelStatus.inactive.channels = channelStatus.inactive.channels + 1;
channelStatus.inactive.capacity = channelStatus.inactive.capacity + channel.toLocal;
}
});
lightningBalances = { localBalance: totalLocalBalance, remoteBalance: totalRemoteBalance };
activeChannels = this.commonService.sortDescByKey(activeChannels, 'balancedness');
this.logger.info('Active Channels: ' + JSON.stringify(activeChannels));
this.logger.info('Pending Channels: ' + JSON.stringify(pendingChannels));
this.logger.info('Inactive Channels: ' + JSON.stringify(inactiveChannels));
this.logger.info('Lightning Balances: ' + JSON.stringify(lightningBalances));
this.logger.info('Channels Status: ' + JSON.stringify(channelStatus));
this.logger.info('Channel, status and balances: ' + JSON.stringify({ active: activeChannels, pending: pendingChannels, inactive: inactiveChannels, balances: lightningBalances, status: channelStatus }));
this.store.dispatch(new ECLActions.SetActiveChannels(activeChannels));
this.store.dispatch(new ECLActions.SetPendingChannels(pendingChannels));
this.store.dispatch(new ECLActions.SetInactiveChannels(inactiveChannels));
this.store.dispatch(new ECLActions.SetLightningBalance(lightningBalances));
this.store.dispatch(new ECLActions.SetChannelsStatus(channelStatus));
}
handleSendPaymentStatus = (msg: string) => {
this.store.dispatch(new ECLActions.UpdateAPICallStatus({ action: 'SendPayment', status: APICallStatusEnum.COMPLETED }));
this.store.dispatch(new RTLActions.CloseSpinner(UI_MESSAGES.SEND_PAYMENT));
this.store.dispatch(new ECLActions.SendPaymentStatus(this.latestPaymentRes));
this.store.dispatch(new ECLActions.FetchChannels({ fetchPayments: true }));
this.store.dispatch(new RTLActions.OpenSnackBar(msg));
};
initializeRemainingData(info: any, landingPage: string) {
this.sessionService.setItem('eclUnlocked', 'true');
const node_data = {

@ -1,5 +1,5 @@
import { SelNodeChild } from '../../shared/models/RTLconfig';
import { GetInfo, Channel, ChannelStats, Fees, OnChainBalance, LightningBalance, Peer, ChannelsStatus, Payments, Transaction, Invoice } from '../../shared/models/eclModels';
import { GetInfo, Channel, Fees, OnChainBalance, LightningBalance, Peer, ChannelsStatus, Payments, Transaction, Invoice, PaymentReceived } from '../../shared/models/eclModels';
import { ApiCallsListECL } from '../../shared/models/apiCallsPayload';
import { APICallStatusEnum, UserPersonaEnum } from '../../shared/services/consts-enums-functions';
import * as ECLActions from './ecl.actions';
@ -13,7 +13,6 @@ export interface ECLState {
pendingChannels: Channel[];
inactiveChannels: Channel[];
channelsStatus: ChannelsStatus;
channelStats: ChannelStats[];
onchainBalance: OnChainBalance;
lightningBalance: LightningBalance;
peers: Peer[];
@ -40,7 +39,6 @@ export const initECLState: ECLState = {
pendingChannels: [],
inactiveChannels: [],
channelsStatus: {},
channelStats: [],
onchainBalance: { total: 0, confirmed: 0, unconfirmed: 0 },
lightningBalance: { localBalance: -1, remoteBalance: -1 },
peers: [],
@ -51,6 +49,11 @@ export const initECLState: ECLState = {
export function ECLReducer(state = initECLState, action: ECLActions.ECLActions) {
switch (action.type) {
case ECLActions.SET_CHILD_NODE_SETTINGS_ECL:
return {
...state,
apiURL: action.payload
};
case ECLActions.UPDATE_API_CALL_STATUS_ECL:
const updatedApisCallStatus = state.apisCallStatus;
updatedApisCallStatus[action.payload.action] = {
@ -104,11 +107,6 @@ export function ECLReducer(state = initECLState, action: ECLActions.ECLActions)
...state,
channelsStatus: action.payload
};
case ECLActions.SET_CHANNEL_STATS_ECL:
return {
...state,
channelStats: action.payload
};
case ECLActions.SET_ONCHAIN_BALANCE_ECL:
return {
...state,
@ -206,11 +204,37 @@ export function ECLReducer(state = initECLState, action: ECLActions.ECLActions)
};
case ECLActions.UPDATE_INVOICE_ECL:
let modifiedInvoices = state.invoices;
modifiedInvoices = modifiedInvoices.map((invoice) => ((invoice.paymentHash === action.payload.paymentHash) ? action.payload : invoice));
modifiedInvoices = modifiedInvoices.map((invoice) => {
if (invoice.paymentHash === action.payload.paymentHash) {
if (action.payload.hasOwnProperty('type')) {
const updatedInvoice = invoice;
updatedInvoice.amountSettled = ((<PaymentReceived>action.payload).parts && (<PaymentReceived>action.payload).parts.length && (<PaymentReceived>action.payload).parts.length > 0 && (<PaymentReceived>action.payload).parts[0].amount) ? (<PaymentReceived>action.payload).parts[0].amount / 1000 : 0;
updatedInvoice.receivedAt = ((<PaymentReceived>action.payload).parts && (<PaymentReceived>action.payload).parts.length && (<PaymentReceived>action.payload).parts.length > 0 && (<PaymentReceived>action.payload).parts[0].timestamp) ? Math.round((<PaymentReceived>action.payload).parts[0].timestamp / 1000) : 0;
updatedInvoice.status = 'received';
return updatedInvoice;
} else {
return action.payload;
}
}
return invoice;
});
return {
...state,
invoices: modifiedInvoices
};
case ECLActions.UPDATE_CHANNEL_STATE_ECL:
let modifiedPendingChannels = state.pendingChannels;
modifiedPendingChannels = modifiedPendingChannels.map((pendingChannel) => {
if (pendingChannel.channelId === action.payload.channelId && pendingChannel.nodeId === action.payload.remoteNodeId) {
action.payload.currentState = action.payload.currentState.replace(/_/g, ' ');
pendingChannel.state = action.payload.currentState;
}
return pendingChannel;
});
return {
...state,
pendingChannels: modifiedPendingChannels
};
default:
return state;
}

@ -115,7 +115,7 @@ export class ECLLightningSendPaymentsComponent implements OnInit, OnDestroy {
}
onPaymentRequestEntry(event: any) {
this.paymentRequest = event;
this.paymentRequest = event && typeof event === 'string' ? event.trim() : event;
this.paymentError = '';
this.paymentDecodedHint = '';
this.zeroAmtInvoice = false;

@ -7,7 +7,7 @@ import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ForwardingEvent } from '../../../shared/models/lndModels';
import { ForwardingEvent, SwitchRes } from '../../../shared/models/lndModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, APICallStatusEnum } from '../../../shared/services/consts-enums-functions';
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
import { LoggerService } from '../../../shared/services/logger.service';
@ -59,18 +59,18 @@ export class ForwardingHistoryComponent implements OnInit, AfterViewInit, OnChan
}
ngOnInit() {
combineLatest([this.store.select(fromLNDReducer.getForwardingHistory), this.store.select(fromLNDReducer.getForwardingHistoryAPIStatus)]).
pipe(takeUntil(this.unSubs[0])).subscribe(([forwardingHistory, apiCallStatus]) => {
this.store.select(fromLNDReducer.forwardingHistoryAndAPIStatus).pipe(takeUntil(this.unSubs[0])).
subscribe((fhSelector: {forwardingHistory: SwitchRes, apisCallStatus: ApiCallStatusPayload}) => {
if (this.eventsData.length <= 0) {
this.errorMessage = '';
this.apisCallStatus = apiCallStatus;
if (apiCallStatus?.status === APICallStatusEnum.ERROR) {
this.apisCallStatus = fhSelector.apisCallStatus;
if (fhSelector.apisCallStatus?.status === APICallStatusEnum.ERROR) {
this.errorMessage = (typeof (this.apisCallStatus.message) === 'object') ? JSON.stringify(this.apisCallStatus.message) : this.apisCallStatus.message;
}
this.forwardingHistoryData = (forwardingHistory?.forwarding_events) ? forwardingHistory.forwarding_events : [];
this.forwardingHistoryData = (fhSelector.forwardingHistory?.forwarding_events) ? fhSelector.forwardingHistory.forwarding_events : [];
this.loadForwardingEventsTable(this.forwardingHistoryData);
this.logger.info(apiCallStatus);
this.logger.info(forwardingHistory);
this.logger.info(fhSelector.apisCallStatus);
this.logger.info(fhSelector.forwardingHistory);
}
});
}

@ -6,7 +6,7 @@ import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { ForwardingEvent, RoutingPeers } from '../../../shared/models/lndModels';
import { ForwardingEvent, RoutingPeers, SwitchRes } from '../../../shared/models/lndModels';
import { AlertTypeEnum, APICallStatusEnum, DataTypeEnum, getPaginatorLabel, PAGE_SIZE, PAGE_SIZE_OPTIONS, ScreenSizeEnum } from '../../../shared/services/consts-enums-functions';
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
import { LoggerService } from '../../../shared/services/logger.service';
@ -64,23 +64,23 @@ export class RoutingPeersComponent implements OnInit, AfterViewInit, OnDestroy {
}
ngOnInit() {
combineLatest([this.store.select(fromLNDReducer.getForwardingHistory), this.store.select(fromLNDReducer.getForwardingHistoryAPIStatus)]).
pipe(takeUntil(this.unSubs[0])).subscribe(([forwardingHistory, apiCallStatus]) => {
this.store.select(fromLNDReducer.forwardingHistoryAndAPIStatus).pipe(takeUntil(this.unSubs[0])).
subscribe((fhSelector: {forwardingHistory: SwitchRes, apisCallStatus: ApiCallStatusPayload}) => {
this.errorMessage = '';
this.apisCallStatus = apiCallStatus;
if (apiCallStatus?.status === APICallStatusEnum.ERROR) {
this.apisCallStatus = fhSelector.apisCallStatus;
if (fhSelector.apisCallStatus?.status === APICallStatusEnum.ERROR) {
this.errorMessage = (typeof (this.apisCallStatus.message) === 'object') ? JSON.stringify(this.apisCallStatus.message) : this.apisCallStatus.message;
}
if (forwardingHistory.forwarding_events) {
this.routingPeersData = forwardingHistory.forwarding_events;
if (fhSelector.forwardingHistory.forwarding_events) {
this.routingPeersData = fhSelector.forwardingHistory.forwarding_events;
} else {
this.routingPeersData = [];
}
if (this.routingPeersData.length > 0 && this.sortIn && this.paginatorIn && this.sortOut && this.paginatorOut) {
this.loadRoutingPeersTable(this.routingPeersData);
}
this.logger.info(apiCallStatus);
this.logger.info(forwardingHistory);
this.logger.info(fhSelector.apisCallStatus);
this.logger.info(fhSelector.forwardingHistory);
});
}

@ -63,6 +63,7 @@ export class LNDEffects implements OnDestroy {
withLatestFrom(this.store.select('root')),
mergeMap(([action, store]: [LNDActions.FetchInfo, fromRTLReducer.RootState]) => {
this.flgInitialized = false;
this.store.dispatch(new RTLActions.SetApiUrl(this.CHILD_API_URL));
this.store.dispatch(new RTLActions.CloseAllDialogs());
this.store.dispatch(new RTLActions.OpenSpinner(UI_MESSAGES.GET_NODE_INFO));
this.store.dispatch(new LNDActions.UpdateAPICallStatus({ action: 'FetchInfo', status: APICallStatusEnum.INITIATED }));

@ -1,5 +1,5 @@
import { pipe } from 'rxjs';
import { scan } from 'rxjs/operators';
import { scan, map } from 'rxjs/operators';
import { createFeatureSelector, createSelector, select } from '@ngrx/store';
import { SelNodeChild } from '../../shared/models/RTLconfig';
import { ApiCallsListLND } from '../../shared/models/apiCallsPayload';
@ -320,6 +320,7 @@ export function LNDReducer(state = initLNDState, action: LNDActions.LNDActions)
export const getLNDState = createFeatureSelector<LNDState>('lnd');
export const getInformation = createSelector(getLNDState, (state: LNDState) => state.information);
export const takeLastGetInfo = (count: number) => pipe(select(getInformation), scan((acc, curr) => [curr, ...acc].filter((val, index) => index < count && val.hasOwnProperty('identity_pubkey')), [] as GetInfo[]));
export const getForwardingHistory = createSelector(getLNDState, (state: LNDState) => state.forwardingHistory);
export const getForwardingHistoryAPIStatus = createSelector(getLNDState, (state: LNDState) => state.apisCallStatus.GetForwardingHistory);
export const takeLastGetInfo = (count: number) => pipe(select(getInformation), scan((acc, curr) => [curr, ...acc].filter((val, index) => index < count && val.hasOwnProperty('identity_pubkey')), [] as GetInfo[]));
export const forwardingHistoryAndAPIStatus = createSelector(getLNDState, (state: LNDState) => ({ forwardingHistory: state.forwardingHistory, apisCallStatus: state.apisCallStatus.GetForwardingHistory }));

@ -92,6 +92,14 @@ export interface PayRequest {
amount?: number;
}
export interface ChannelsRearranged {
activeChannels?: Channel[];
pendingChannels?: Channel[];
inactiveChannels?: Channel[];
lightningBalances?: LightningBalance;
channelStatus?: ChannelsStatus;
}
export interface Channel {
alias?: string;
nodeId?: string;
@ -110,14 +118,6 @@ export interface Channel {
balancedness?: number;
}
export interface ChannelStats {
channelId?: string;
avgPaymentAmount?: number;
paymentCount?: number;
relayFee?: number;
networkFee?: number;
}
export interface OnChainBalance {
total?: number;
confirmed?: number;
@ -209,3 +209,11 @@ export interface RoutingPeers {
totalAmount?: number;
totalFee?: number;
}
export interface ChannelStateUpdate {
channelId?: string;
currentState?: string;
previousState?: string;
remoteNodeId?: string;
type?: string;
}

@ -48,7 +48,6 @@ export const FEE_RATE_TYPES = [
{ feeRateId: 'customperkb', feeRateType: 'Custom (Sats/vB)' }
];
export const NODE_SETTINGS = {
themes: [
{ id: 'PURPLE', name: 'Diogo' },
@ -60,6 +59,28 @@ export const NODE_SETTINGS = {
modes: [{ id: 'DAY', name: 'Day' }, { id: 'NIGHT', name: 'Night' }]
};
export enum WSEventTypeEnum {
PAYMENT_RECEIVED = 'payment-received',
PAYMENT_RELAYED = 'payment-relayed',
PAYMENT_SENT = 'payment-sent',
PAYMENT_SETTLING_ONCHAIN = 'payment-settling-onchain',
PAYMENT_FAILED = 'payment-failed',
CHANNEL_OPENED = 'channel-opened',
CHANNEL_STATE_CHANGED = 'channel-state-changed',
CHANNEL_CLOSED = 'channel-closed'
}
export const WSEventsECL = [
{ type: WSEventTypeEnum.PAYMENT_RECEIVED, name: 'Payment Received', description: 'A payment has been received' },
{ type: WSEventTypeEnum.PAYMENT_RELAYED, name: 'Payment Relayed', description: 'A payment has been successfully relayed' },
{ type: WSEventTypeEnum.PAYMENT_SENT, name: 'Payment Sent', description: 'A payment has been successfully sent' },
{ type: WSEventTypeEnum.PAYMENT_SETTLING_ONCHAIN, name: 'Payment Settling Onchain', description: 'A payment was not fulfilled and its HTLC is being redeemed on-chain' },
{ type: WSEventTypeEnum.PAYMENT_FAILED, name: 'Payment Failed', description: 'A payment failed' },
{ type: WSEventTypeEnum.CHANNEL_OPENED, name: 'Channel Opened', description: 'A channel opening flow has started' },
{ type: WSEventTypeEnum.CHANNEL_STATE_CHANGED, name: 'Channel State Changed', description: 'A channel state changed' },
{ type: WSEventTypeEnum.CHANNEL_CLOSED, name: 'Channel Closed', description: 'A channel has been closed' }
];
export enum UserPersonaEnum {
OPERATOR = 'OPERATOR',
MERCHANT = 'MERCHANT',

@ -0,0 +1,90 @@
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { WebSocketSubject } from 'rxjs/webSocket';
import { LoggerService } from '../../shared/services/logger.service';
import { WSEventTypeEnum } from './consts-enums-functions';
import { SessionService } from './session.service';
@Injectable()
export class WebSocketClientService implements OnDestroy {
public wsMessages: BehaviorSubject<any> = new BehaviorSubject(null);
private prevMessage = {};
private wsUrl = '';
private socket: WebSocketSubject<any> | null;
private RETRY_SECONDS = 5;
private RECONNECT_TIMEOUT = null;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private sessionService: SessionService) {}
connectWebSocket(finalWSUrl: string) {
this.wsUrl = finalWSUrl;
this.logger.info('Websocket Url: ' + this.wsUrl);
if (!this.socket || this.socket.closed) {
this.socket = new WebSocketSubject({
url: finalWSUrl,
protocol: [this.sessionService.getItem('token')]
});
this.subscribeToMessages();
}
}
reconnectOnError() {
if (this.RECONNECT_TIMEOUT) { return; }
this.RETRY_SECONDS = (this.RETRY_SECONDS >= 160) ? 160 : (this.RETRY_SECONDS * 2);
this.RECONNECT_TIMEOUT = setTimeout(() => {
this.logger.info('Reconnecting Web Socket.');
this.connectWebSocket(this.wsUrl);
this.RECONNECT_TIMEOUT = null;
}, this.RETRY_SECONDS * 1000);
}
closeConnection() {
if (this.socket) {
this.socket.complete();
this.socket = null;
}
}
sendMessage(msg: any) {
if (this.socket) {
const payload = { token: 'token_from_session_service', message: msg };
this.socket.next(payload);
}
}
private subscribeToMessages() {
this.socket.pipe(takeUntil(this.unSubs[1])).subscribe({
next: (msg) => {
msg = (typeof msg === 'string') ? JSON.parse(msg) : msg;
if (msg.error) {
this.handleError(msg.error);
} else {
const msgStr = JSON.stringify(msg);
if (this.prevMessage.hasOwnProperty(msg.type) && this.prevMessage[msg.type] === msgStr) { return; }
this.prevMessage[msg.type] = msgStr;
this.logger.info('Next Message from WS:' + JSON.stringify(msg));
this.wsMessages.next(msg);
}
},
error: (err) => this.handleError(err),
complete: () => { this.logger.info('Web Socket Closed'); }
});
}
private handleError(err) {
this.logger.error(err);
this.wsMessages.error(err);
this.reconnectOnError();
}
ngOnDestroy() {
this.closeConnection();
this.wsMessages.next(null);
this.wsMessages.complete();
}
}

@ -158,9 +158,9 @@ body {
}
.padding-gap-x-large {
padding: 0 ($gap*4) 0 ($gap*4) !important;
padding: 0 ($gap*2) 0 ($gap*2) !important;
@include for_screensize(tab-land) {
padding: 0 ($gap*2) 0 ($gap*2) !important;
padding: 0 ($gap) 0 ($gap) !important;
}
@include for_screensize(tab-port) {
padding: 0 $gap/2 0 $gap/2 !important;

@ -6,6 +6,7 @@ import { RTLConfiguration, Settings, ConfigSettingsNode, GetInfoRoot, SSO } from
import { ServicesEnum } from '../shared/services/consts-enums-functions';
export const VOID = 'VOID';
export const SET_API_URL_ECL = 'SET_API_URL_ECL';
export const UPDATE_SELECTED_NODE_OPTIONS = 'UPDATE_SELECTED_NODE_OPTIONS';
export const UPDATE_API_CALL_STATUS_ROOT = 'UPDATE_API_CALL_STATUS_ROOT';
export const RESET_ROOT_STORE = 'RESET_ROOT_STORE';
@ -46,6 +47,13 @@ export class VoidAction implements Action {
}
export class SetApiUrl implements Action {
readonly type = SET_API_URL_ECL;
constructor(public payload: string) {}
}
export class UpdateAPICallStatus implements Action {
readonly type = UPDATE_API_CALL_STATUS_ROOT;
@ -260,7 +268,7 @@ export class ShowFile implements Action {
}
export type RTLActions = UpdateAPICallStatus | IsAuthorized | IsAuthorizedRes | Login | VerifyTwoFA |
export type RTLActions = UpdateAPICallStatus | SetApiUrl | IsAuthorized | IsAuthorizedRes | Login | VerifyTwoFA |
VoidAction | CloseAllDialogs | OpenSnackBar | OpenSpinner | CloseSpinner | FetchRTLConfig | SetRTLConfig | SaveSettings |
OpenAlert | CloseAlert | OpenConfirmation | CloseConfirmation | ShowPubkey | FetchConfig | ShowConfig |
UpdateSelectedNodeOptions | ResetRootStore |

@ -9,7 +9,8 @@ import { map, mergeMap, catchError, take, withLatestFrom, takeUntil } from 'rxjs
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { environment } from '../../environments/environment';
import { environment, API_URL } from '../../environments/environment';
import { WebSocketClientService } from '../shared/services/web-socket.service';
import { LoggerService } from '../shared/services/logger.service';
import { SessionService } from '../shared/services/session.service';
import { CommonService } from '../shared/services/common.service';
@ -43,6 +44,7 @@ export class RTLEffects implements OnDestroy {
private httpClient: HttpClient,
private store: Store<fromRTLReducer.RTLState>,
private logger: LoggerService,
private wsService: WebSocketClientService,
private sessionService: SessionService,
private commonService: CommonService,
private dataService: DataService,
@ -200,12 +202,12 @@ export class RTLEffects implements OnDestroy {
this.store.dispatch(new RTLActions.OpenSpinner(UI_MESSAGES.GET_RTL_CONFIG));
this.store.dispatch(new RTLActions.UpdateAPICallStatus({ action: 'FetchRTLConfig', status: APICallStatusEnum.INITIATED }));
if (this.sessionService.getItem('token')) {
return this.httpClient.get(environment.CONF_API + '/rtlconf');
return this.httpClient.get<RTLConfiguration>(environment.CONF_API + '/rtlconf');
} else {
return this.httpClient.get(environment.CONF_API + '/rtlconfinit');
return this.httpClient.get<RTLConfiguration>(environment.CONF_API + '/rtlconfinit');
}
}),
map((rtlConfig: any) => {
map((rtlConfig: RTLConfiguration) => {
this.logger.info(rtlConfig);
this.store.dispatch(new RTLActions.CloseSpinner(UI_MESSAGES.GET_RTL_CONFIG));
this.store.dispatch(new RTLActions.UpdateAPICallStatus({ action: 'FetchRTLConfig', status: APICallStatusEnum.COMPLETED }));
@ -531,6 +533,7 @@ export class RTLEffects implements OnDestroy {
);
initializeNode(node: any, isInitialSetup: boolean) {
this.logger.info('Initializing node from RTL Effects.');
const landingPage = isInitialSetup ? '' : 'HOME';
let selNode = {};
if (node.settings.fiatConversion && node.settings.currencyUnit) {
@ -546,6 +549,8 @@ export class RTLEffects implements OnDestroy {
this.store.dispatch(new CLActions.ResetCLStore(selNode));
this.store.dispatch(new ECLActions.ResetECLStore(selNode));
if (this.sessionService.getItem('token')) {
const apiUrl = (environment.production && window.location.origin) ? (window.location.origin + '/rtl/api') : API_URL;
this.wsService.connectWebSocket(apiUrl.replace(/^http/, 'ws') + environment.Web_SOCKET_API);
node.lnImplementation = node.lnImplementation.toUpperCase();
this.dataService.setChildAPIUrl(node.lnImplementation);
switch (node.lnImplementation) {

@ -1,4 +1,4 @@
import { ActionReducerMap } from '@ngrx/store';
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ApiCallsListRoot } from '../shared/models/apiCallsPayload';
import { APICallStatusEnum } from '../shared/services/consts-enums-functions';
@ -10,6 +10,7 @@ import * as fromLND from '../lnd/store/lnd.reducers';
import * as RTLActions from './rtl.actions';
export interface RootState {
apiURL: string;
apisCallStatus: ApiCallsListRoot;
selNode: ConfigSettingsNode;
appConfig: RTLConfiguration;
@ -20,6 +21,7 @@ const initNodeSettings = { userPersona: 'OPERATOR', themeMode: 'DAY', themeColor
const initNodeAuthentication = { configPath: '', swapMacaroonPath: '', boltzMacaroonPath: '' };
export const initRootState: RootState = {
apiURL: '',
apisCallStatus: { Login: { status: APICallStatusEnum.UN_INITIATED }, IsAuthorized: { status: APICallStatusEnum.UN_INITIATED } },
selNode: { settings: initNodeSettings, authentication: initNodeAuthentication, lnImplementation: 'LND' },
appConfig: {
@ -80,9 +82,12 @@ export interface RTLState {
ecl: fromECL.ECLState;
}
export const RTLReducer: ActionReducerMap<RTLState> = {
export const RTLReducer = {
root: RootReducer,
lnd: fromLND.LNDReducer,
cl: fromCL.CLReducer,
ecl: fromECL.ECLReducer
};
export const getRTLState = createFeatureSelector<RTLState>('rtl');
export const getApiUrl = createSelector(getRTLState, (state: RTLState) => state.root.apiURL);

@ -25,5 +25,6 @@ export const environment = {
MESSAGE_API: '/message',
LOOP_API: '/loop',
BOLTZ_API: '/boltz',
Web_SOCKET_API: '/ws',
VERSION: VERSION
};

@ -25,5 +25,6 @@ export const environment = {
MESSAGE_API: '/message',
LOOP_API: '/loop',
BOLTZ_API: '/boltz',
Web_SOCKET_API: '/ws',
VERSION: VERSION
};

Loading…
Cancel
Save