Release 0.12.2 (#964)

Release 0.12.2.

Co-authored-by: saiy2k <saiy2k@gmail.com>
Co-authored-by: bota87 <52842374+bota87@users.noreply.github.com>
Co-authored-by: Bota <Bota@ASUS>
Co-authored-by: Harshvardhan R <harshvardhanr.181it217@nitk.edu.in>
pull/969/head
ShahanaFarooqui 2 years ago committed by GitHub
parent 9c59954205
commit 3910284d58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -59,7 +59,7 @@ CONFIG_PATH (Full path of the LNP .conf file including the file name) (Optional
MACAROON_PATH (Path for the folder containing 'admin.macaroon' (LND)/'access.macaroon' (CLT) file, Required for LND & CLT)<br />
SWAP_MACAROON_PATH (Path for the folder containing Loop's 'loop.macaroon', optional)<br />
BOLTZ_MACAROON_PATH (Path for the folder containing Boltz's 'admin.macaroon', optional)<br />
RTL_SSO (1 - single sign on via an external cookie, 0 - stand alone RTL authentication, Optional)<br />
RTL_SSO (1 - single sign on via an external cookie, 0 - stand alone RTL authentication, Required)<br />
RTL_COOKIE_PATH (Full path of the cookie file including the file name, Required if RTL_SSO=1 else Optional)<br />
LOGOUT_REDIRECT_LINK (URL to re-direct to after logout/timeout from RTL, Required if RTL_SSO=1 else Optional)<br />
RTL_CONFIG_PATH (Path for the folder containing 'RTL-Config.json' file, Required)<br />

@ -32,10 +32,7 @@ To download from master (*not recommended*):
```
$ git clone https://github.com/Ride-The-Lightning/RTL.git
$ cd RTL
$ npm install
$ npm run buildfrontend
$ npm run buildbackend
$ npm prune --production
$ npm install --only=prod
```
#### Or: Update existing build
@ -44,10 +41,7 @@ $ cd RTL
$ git reset --hard HEAD
$ git clean -f -d
$ git pull
$ npm install
$ npm run buildfrontend
$ npm run buildbackend
$ npm prune --production
$ npm install --only=prod
```
### <a name="prep"></a>Prep for Execution
RTL requires its own config file `RTL-Config.json`, to start the server and provide user authentication on the app.

@ -40,10 +40,10 @@ Design suggestions are always welcome and helpful. Design suggestion can range f
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
* Pull the code from the release (current eg Release-0.11.1) or [master](https://github.com/Ride-The-Lightning/RTL/tree/master) branch into your local workspace via github commandline/GUI.
* Pull the code from the release (current eg Release-0.12.2) branch into your local workspace via github commandline/GUI.
##### Install Dependencies
* Assuming that nodejs (v12 & above) and npm are already installed on your local machine. Go into your RTL root folder and run `npm install`.
* Assuming that nodejs (v14 & above) and npm are already installed on your local machine. Go into your RTL root folder and run `npm install`.
* Sometimes after installation, user receives a message from npm to fix dependency vulnerability by running `npm audit fix`. Please do not follow this step as it can break some of the working RTL code on your machine. We audit and fix these vulnerabilities as soon as possible at our end.
##### Node Backend Server for Development
@ -70,7 +70,7 @@ Contributions via code is the most sought after contribution and something we en
##### Create a Pull Request
* Create a new branch on the github to push your updated code.
* Commit your updates into the newly created branch.
* Create a new pull request once you are satisfied with your updates to be merged into the `release`/`master` branch with details of your updates and submit it for the review.
* Create a new pull request once you are satisfied with your updates to be merged into the latest `release` branch with details of your updates and submit it for the review.
##### Caution about adding new libraries
* We are conservative in adding new dependencies to the repository. Do your best to not add any new libraries on RTL. We believe this is the best strategy to keep the software safe from vulnerabilites.

@ -28,10 +28,7 @@ To download from master (*not recommended*) follow the below instructions:
```
$ git clone https://github.com/Ride-The-Lightning/RTL.git
$ cd RTL
$ npm install
$ npm run buildfrontend
$ npm run buildbackend
$ npm prune --production
$ npm install --only=prod
```
#### Or: Update existing build
```
@ -39,10 +36,7 @@ $ cd RTL
$ git reset --hard HEAD
$ git clean -f -d
$ git pull
$ npm install
$ npm run buildfrontend
$ npm run buildbackend
$ npm prune --production
$ npm install --only=prod
```
### <a name="prep"></a>Prep for Execution
RTL requires its own config file `RTL-Config.json`, to start the server and provide user authentication on the app.

@ -35,9 +35,6 @@ export const arrangePayments = (selNode, body) => {
if (sentEle.recipientAmount) {
sentEle.recipientAmount = Math.round(sentEle.recipientAmount / 1000);
}
if (sentEle.parts && sentEle.parts.length > 0) {
sentEle.firstPartTimestamp = sentEle.parts[0].timestamp;
}
sentEle.parts.forEach((part) => {
if (part.amount) {
part.amount = Math.round(part.amount / 1000);
@ -45,19 +42,31 @@ export const arrangePayments = (selNode, body) => {
if (part.feesPaid) {
part.feesPaid = Math.round(part.feesPaid / 1000);
}
if (part.timestamp.unix) {
part.timestamp = part.timestamp.unix * 1000;
}
});
if (sentEle.parts && sentEle.parts.length > 0) {
sentEle.firstPartTimestamp = sentEle.parts[0].timestamp;
}
});
payments.received.forEach((receivedEle) => {
if (receivedEle.parts && receivedEle.parts.length > 0) {
receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp;
}
receivedEle.parts.forEach((part) => {
if (part.amount) {
part.amount = Math.round(part.amount / 1000);
}
if (part.timestamp.unix) {
part.timestamp = part.timestamp.unix * 1000;
}
});
if (receivedEle.parts && receivedEle.parts.length > 0) {
receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp;
}
});
payments.relayed.forEach((relayedEle) => {
if (relayedEle.timestamp.unix) {
relayedEle.timestamp = relayedEle.timestamp.unix * 1000;
}
if (relayedEle.amountIn) {
relayedEle.amountIn = Math.round(relayedEle.amountIn / 1000);
}

@ -42,8 +42,7 @@ export class ConfigService {
channelBackupPath = '';
break;
}
return {
multiPass: 'password',
const configData = {
port: '3000',
defaultNodeIndex: 1,
SSO: {
@ -72,6 +71,10 @@ export class ConfigService {
}
]
};
if (+process.env.RTL_SSO === 0) {
configData['multiPass'] = 'password';
}
return configData;
};
this.normalizePort = (val) => {
const port = parseInt(val, 10);
@ -122,7 +125,7 @@ export class ConfigService {
this.common.rtl_secret2fa = config.secret2fa;
}
else {
if ((process.env.APP_PASSWORD && process.env.APP_PASSWORD.trim() !== '') || (config.multiPass && config.multiPass.trim() !== '') || (config.multiPassHashed && config.multiPassHashed.trim() !== '')) {
if (process.env.APP_PASSWORD && process.env.APP_PASSWORD.trim() !== '') {
this.errMsg = this.errMsg + '\nRTL Password cannot be set with SSO. Please set SSO as 0 or remove password.';
}
}

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

@ -13,6 +13,6 @@
<style>@font-face{font-family:Roboto;src:url(Roboto-Thin.f7a95c9c5999532c.woff2) format("woff2"),url(Roboto-Thin.c13c157cb81e8ebb.woff) format("woff");font-weight:100;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.b0e084abf689f393.woff2) format("woff2"),url(Roboto-ThinItalic.1111028df6cea564.woff) format("woff");font-weight:100;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Light.0e01b6cd13b3857f.woff2) format("woff2"),url(Roboto-Light.603ca9a537b88428.woff) format("woff");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.232ef4b20215f720.woff2) format("woff2"),url(Roboto-LightItalic.1b5e142f787151c8.woff) format("woff");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Regular.475ba9e4e2d63456.woff2) format("woff2"),url(Roboto-Regular.bcefbfee882bc1cb.woff) format("woff");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.e3a9ebdaac06bbc4.woff2) format("woff2"),url(Roboto-RegularItalic.0668fae6af0cf8c2.woff) format("woff");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Medium.457532032ceb0168.woff2) format("woff2"),url(Roboto-Medium.6e1ae5f0b324a0aa.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.872f7060602d55d2.woff2) format("woff2"),url(Roboto-MediumItalic.e06fb533801cbb08.woff) format("woff");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Bold.447291a88c067396.woff2) format("woff2"),url(Roboto-Bold.fc482e6133cf5e26.woff) format("woff");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.1b15168ef6fa4e16.woff2) format("woff2"),url(Roboto-BoldItalic.e26ba339b06f09f7.woff) format("woff");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Black.2eaa390d458c877d.woff2) format("woff2"),url(Roboto-Black.b25f67ad8583da68.woff) format("woff");font-weight:900;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.7dc03ee444552bc5.woff2) format("woff2"),url(Roboto-BlackItalic.c8dc642467cb3099.woff) format("woff");font-weight:900;font-style:italic}html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:62.5%}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}</style><link rel="stylesheet" href="styles.30265dd456248897.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.30265dd456248897.css"></noscript></head>
<body>
<rtl-app></rtl-app>
<script src="runtime.ba1141771472db99.js" type="module"></script><script src="polyfills.6d989da208bd6fd1.js" type="module"></script><script src="main.d30092e5998e5338.js" type="module"></script>
<script src="runtime.f8bc6cf2eae33aff.js" type="module"></script><script src="polyfills.6d989da208bd6fd1.js" type="module"></script><script src="main.b20278fd7f5f9167.js" type="module"></script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
(()=>{"use strict";var e,v={},g={};function r(e){var i=g[e];if(void 0!==i)return i.exports;var t=g[e]={id:e,loaded:!1,exports:{}};return v[e].call(t.exports,t,t.exports,r),t.loaded=!0,t.exports}r.m=v,e=[],r.O=(i,t,f,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,f,o]=e[n],s=!0,u=0;u<t.length;u++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[u]))?t.splice(u--,1):(s=!1,o<a&&(a=o));if(s){e.splice(n--,1);var d=f();void 0!==d&&(i=d)}}return i}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,f,o]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>e+"."+{632:"2ee91869a10494c2",637:"d1905636b60ffa80",859:"88e2ffd83f4b1f85",893:"bc205686870d1329"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="RTLApp:";r.l=(t,f,o,n)=>{if(e[t])e[t].push(f);else{var a,s;if(void 0!==o)for(var u=document.getElementsByTagName("script"),d=0;d<u.length;d++){var l=u[d];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==i+o){a=l;break}}a||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+o),a.src=r.tu(t)),e[t]=[f];var c=(m,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(_=>_(b)),m)return m(b)},p=setTimeout(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(f,o)=>{var n=r.o(e,f)?e[f]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=f){var a=new Promise((l,c)=>n=e[f]=[l,c]);o.push(n[2]=a);var s=r.p+r.u(f),u=new Error;r.l(s,l=>{if(r.o(e,f)&&(0!==(n=e[f])&&(e[f]=void 0),n)){var c=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;u.message="Loading chunk "+f+" failed.\n("+c+": "+p+")",u.name="ChunkLoadError",u.type=c,u.request=p,n[1](u)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var i=(f,o)=>{var u,d,[n,a,s]=o,l=0;if(n.some(p=>0!==e[p])){for(u in a)r.o(a,u)&&(r.m[u]=a[u]);if(s)var c=s(r)}for(f&&f(o);l<n.length;l++)r.o(e,d=n[l])&&e[d]&&e[d][0](),e[n[l]]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();

@ -0,0 +1 @@
(()=>{"use strict";var e,v={},g={};function r(e){var i=g[e];if(void 0!==i)return i.exports;var t=g[e]={id:e,loaded:!1,exports:{}};return v[e].call(t.exports,t,t.exports,r),t.loaded=!0,t.exports}r.m=v,e=[],r.O=(i,t,f,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,f,o]=e[n],c=!0,u=0;u<t.length;u++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[u]))?t.splice(u--,1):(c=!1,o<a&&(a=o));if(c){e.splice(n--,1);var d=f();void 0!==d&&(i=d)}}return i}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,f,o]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>e+"."+{37:"d8c372593cfce7d0",634:"eb0b1dccb37a5b7b",637:"ab593ccfc00736f3",893:"050b2235bfbe164a"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="RTLApp:";r.l=(t,f,o,n)=>{if(e[t])e[t].push(f);else{var a,c;if(void 0!==o)for(var u=document.getElementsByTagName("script"),d=0;d<u.length;d++){var l=u[d];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==i+o){a=l;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+o),a.src=r.tu(t)),e[t]=[f];var s=(m,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(_=>_(b)),m)return m(b)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(f,o)=>{var n=r.o(e,f)?e[f]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=f){var a=new Promise((l,s)=>n=e[f]=[l,s]);o.push(n[2]=a);var c=r.p+r.u(f),u=new Error;r.l(c,l=>{if(r.o(e,f)&&(0!==(n=e[f])&&(e[f]=void 0),n)){var s=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;u.message="Loading chunk "+f+" failed.\n("+s+": "+p+")",u.name="ChunkLoadError",u.type=s,u.request=p,n[1](u)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var i=(f,o)=>{var u,d,[n,a,c]=o,l=0;if(n.some(p=>0!==e[p])){for(u in a)r.o(a,u)&&(r.m[u]=a[u]);if(c)var s=c(r)}for(f&&f(o);l<n.length;l++)r.o(e,d=n[l])&&e[d]&&e[d][0](),e[n[l]]=0;return r.O(s)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();

2
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "rtl",
"version": "0.12.1-beta",
"version": "0.12.2-beta",
"lockfileVersion": 2,
"requires": true,
"packages": {

@ -1,6 +1,6 @@
{
"name": "rtl",
"version": "0.12.1-beta",
"version": "0.12.2-beta",
"license": "MIT",
"type": "module",
"scripts": {

@ -36,23 +36,26 @@ export const arrangePayments = (selNode: CommonSelectedNode, body) => {
};
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;
}
sentEle.parts.forEach((part) => {
if (part.amount) { part.amount = Math.round(part.amount / 1000); }
if (part.feesPaid) { part.feesPaid = Math.round(part.feesPaid / 1000); }
if (part.timestamp.unix) { part.timestamp = part.timestamp.unix * 1000; }
});
if (sentEle.parts && sentEle.parts.length > 0) {
sentEle.firstPartTimestamp = sentEle.parts[0].timestamp;
}
});
payments.received.forEach((receivedEle) => {
if (receivedEle.parts && receivedEle.parts.length > 0) {
receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp;
}
receivedEle.parts.forEach((part) => {
if (part.amount) { part.amount = Math.round(part.amount / 1000); }
if (part.timestamp.unix) { part.timestamp = part.timestamp.unix * 1000; }
});
if (receivedEle.parts && receivedEle.parts.length > 0) {
receivedEle.firstPartTimestamp = receivedEle.parts[0].timestamp;
}
});
payments.relayed.forEach((relayedEle) => {
if (relayedEle.timestamp.unix) { relayedEle.timestamp = relayedEle.timestamp.unix * 1000; }
if (relayedEle.amountIn) { relayedEle.amountIn = Math.round(relayedEle.amountIn / 1000); }
if (relayedEle.amountOut) { relayedEle.amountOut = Math.round(relayedEle.amountOut / 1000); }
});

@ -46,8 +46,7 @@ export class ConfigService {
channelBackupPath = '';
break;
}
return {
multiPass: 'password',
const configData = {
port: '3000',
defaultNodeIndex: 1,
SSO: {
@ -76,6 +75,10 @@ export class ConfigService {
}
]
};
if (+process.env.RTL_SSO === 0) {
configData['multiPass'] = 'password';
}
return configData;
};
private normalizePort = (val) => {
@ -124,7 +127,7 @@ export class ConfigService {
}
this.common.rtl_secret2fa = config.secret2fa;
} else {
if ((process.env.APP_PASSWORD && process.env.APP_PASSWORD.trim() !== '') || (config.multiPass && config.multiPass.trim() !== '') || (config.multiPassHashed && config.multiPassHashed.trim() !== '')) {
if (process.env.APP_PASSWORD && process.env.APP_PASSWORD.trim() !== '') {
this.errMsg = this.errMsg + '\nRTL Password cannot be set with SSO. Please set SSO as 0 or remove password.';
}
}

@ -156,9 +156,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
this.commonService.setContainerSize(this.sideNavContent.elementRef.nativeElement.clientWidth, this.sideNavContent.elementRef.nativeElement.clientHeight);
} else {
setTimeout(() => {
if (this.flgLoggedIn) {
this.renderer.setStyle(this.sideNavContent.elementRef.nativeElement, 'marginLeft', '22rem'); // $regular-sidenav-width
}
this.renderer.setStyle(this.sideNavContent.elementRef.nativeElement, 'marginLeft', '22rem'); // $regular-sidenav-width
this.commonService.setContainerSize(this.sideNavContent.elementRef.nativeElement.clientWidth, this.sideNavContent.elementRef.nativeElement.clientHeight);
}, 100);
}

@ -20,6 +20,7 @@ import { CLLookupsComponent } from './graph/lookups/lookups.component';
import { CLRoutingComponent } from './routing/routing.component';
import { CLForwardingHistoryComponent } from './routing/forwarding-history/forwarding-history.component';
import { CLFailedTransactionsComponent } from './routing/failed-transactions/failed-transactions.component';
import { CLLocalFailedTransactionsComponent } from './routing/local-failed-transactions/local-failed-transactions.component';
import { CLRoutingPeersComponent } from './routing/routing-peers/routing-peers.component';
import { CLChannelLookupComponent } from './graph/lookups/channel-lookup/channel-lookup.component';
import { CLNodeLookupComponent } from './graph/lookups/node-lookup/node-lookup.component';
@ -76,6 +77,7 @@ import { CLUnlockedGuard } from '../shared/services/auth.guard';
CLRoutingComponent,
CLForwardingHistoryComponent,
CLFailedTransactionsComponent,
CLLocalFailedTransactionsComponent,
CLRoutingPeersComponent,
CLChannelLookupComponent,
CLNodeLookupComponent,

@ -33,6 +33,7 @@ import { NotFoundComponent } from '../shared/components/not-found/not-found.comp
import { CLGraphComponent } from './graph/graph.component';
import { CLOffersTableComponent } from './transactions/offers/offers-table/offers-table.component';
import { CLOfferBookmarksTableComponent } from './transactions/offers/offer-bookmarks-table/offer-bookmarks-table.component';
import { CLLocalFailedTransactionsComponent } from './routing/local-failed-transactions/local-failed-transactions.component';
export const ClRoutes: Routes = [
{
@ -82,6 +83,7 @@ export const ClRoutes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'forwardinghistory' },
{ path: 'forwardinghistory', component: CLForwardingHistoryComponent, canActivate: [CLUnlockedGuard] },
{ path: 'failedtransactions', component: CLFailedTransactionsComponent, canActivate: [CLUnlockedGuard] },
{ path: 'localfail', component: CLLocalFailedTransactionsComponent, canActivate: [CLUnlockedGuard] },
{ path: 'routingpeers', component: CLRoutingPeersComponent, canActivate: [CLUnlockedGuard] }
]
},

@ -15,7 +15,9 @@
<div class="channels-capacity-scroll" [perfectScrollbar]>
<div fxLayout="column" fxFlex="100" *ngIf="activeChannels && activeChannels.length > 0; else noChannelBlock">
<div *ngFor="let channel of activeChannels" class="mt-2">
<span class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.id}}" matTooltipDisabled="{{(channel.alias || channel.id).length < 26}}">{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}}</span>
<a [routerLink]="['../connections/channels/open']" [state]="{filter: channel.id}" class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.id}}" matTooltipDisabled="{{(channel.alias || channel.id).length < 26}}">
{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}}
</a>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint fxFlex="40" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Local:</strong>{{channel.msatoshi_to_us/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint fxFlex="20" fxLayoutAlign="center center" class="font-size-90 color-primary">

@ -8,7 +8,9 @@
<div [perfectScrollbar] fxLayout="column" fxFlex.gt-sm="88" fxFlex="84" fxLayoutAlign="start start">
<div fxLayout="column" fxFlex="100" class="w-100" *ngIf="activeChannels && activeChannels.length > 0; else noChannelBlock">
<div *ngFor="let channel of activeChannels" class="mt-2">
<span class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.id}}" matTooltipDisabled="{{(channel.alias || channel.id).length < 26}}">{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}}</span>
<a [routerLink]="['../connections/channels/open']" [state]="{filter: channel.id}" class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.id}}" matTooltipDisabled="{{(channel.alias || channel.id).length < 26}}">
{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}}
</a>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint *ngIf="direction === 'In'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.msatoshi_to_them/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint *ngIf="direction === 'Out'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.msatoshi_to_us/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>

@ -13,12 +13,11 @@
<span>{{card.title}}</span>
</div>
<div>
<button *ngIf="card.link" mat-icon-button class="more-button" [matMenuTriggerFor]="menuOperator" aria-label="Toggle menu">
<button *ngIf="card.links[0]" mat-icon-button class="more-button" [matMenuTriggerFor]="menuOperator" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuOperator="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button *ngIf="card.id === 'fee'" (click)="onNavigateTo('/cl/reports')" mat-menu-item>Fees Summary</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
<button *ngIf="card.id === 'capacity'" (click)="onsortChannelsBy()" mat-menu-item>Sort By {{sortField === 'Balance Score' ? 'Capacity' : 'Balance Score'}}</button>
</mat-menu>
</div>
@ -66,11 +65,11 @@
<span>{{card.title}}</span>
</div>
<div>
<button *ngIf="card.link" mat-icon-button class="more-button" [matMenuTriggerFor]="menuMerchant" aria-label="Toggle menu">
<button *ngIf="card.links[0]" mat-icon-button class="more-button" [matMenuTriggerFor]="menuMerchant" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuMerchant="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
</mat-menu>
</div>
</mat-card-title>
@ -100,8 +99,7 @@
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuTransactions="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button mat-menu-item (click)="onNavigateTo('/cl/reports/transactions')">Transactions Summary</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
</mat-menu>
</ng-template>
</mat-tab>

@ -17,6 +17,16 @@ import { CommonService } from '../../shared/services/common.service';
import { RTLState } from '../../store/rtl.state';
import { balance, channels, fees, localRemoteBalance, nodeInfoAndNodeSettingsAndAPIsStatus } from '../store/cl.selector';
export interface Tile {
id: string;
title: string;
cols: number;
rows: number;
goToOptions?: string[];
links?: string[];
icon?: any;
}
@Component({
selector: 'rtl-cl-home',
templateUrl: './home.component.html',
@ -46,8 +56,8 @@ export class CLHomeComponent implements OnInit, OnDestroy {
public allOutboundChannels: Channel[] = [];
public totalInboundLiquidity = 0;
public totalOutboundLiquidity = 0;
public operatorCards = [];
public merchantCards = [];
public operatorCards: Tile[] = [];
public merchantCards: Tile[] = [];
public screenSize = '';
public operatorCardHeight = '330px';
public merchantCardHeight = '65px';
@ -66,45 +76,45 @@ export class CLHomeComponent implements OnInit, OnDestroy {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 10, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/cl/onchain', icon: this.faChartPie, title: 'Balances', cols: 10, rows: 1 },
{ id: 'fee', goTo: 'Routing', link: '/cl/routing', icon: this.faBolt, title: 'Routing Fee', cols: 10, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/cl/connections', icon: this.faNetworkWired, title: 'Channels', cols: 10, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/cl/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 10, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 10, rows: 1 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 10, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'connections/channels/pending'], icon: this.faNetworkWired, title: 'Channels', cols: 10, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/cl/onchain', icon: this.faChartPie, title: 'Balances', cols: 6, rows: 4 },
{ id: 'transactions', goTo: 'Transactions', link: '/cl/transactions', title: '', cols: 6, rows: 4 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/cl/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 6, rows: 8 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/cl/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 6, rows: 8 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 6, rows: 4 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 6, rows: 4 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 6, rows: 8 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 6, rows: 8 }
];
} else if (this.screenSize === ScreenSizeEnum.SM || this.screenSize === ScreenSizeEnum.MD) {
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 5, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/cl/onchain', icon: this.faChartPie, title: 'Balances', cols: 5, rows: 1 },
{ id: 'fee', goTo: 'Routing', link: '/cl/routing', icon: this.faBolt, title: 'Routing Fee', cols: 5, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/cl/connections', icon: this.faNetworkWired, title: 'Channels', cols: 5, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/cl/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 5, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 5, rows: 1 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 5, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'connections/channels/pending'], icon: this.faNetworkWired, title: 'Channels', cols: 5, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/cl/onchain', icon: this.faChartPie, title: 'Balances', cols: 3, rows: 4 },
{ id: 'transactions', goTo: 'Transactions', link: '/cl/transactions', title: '', cols: 3, rows: 4 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/cl/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 3, rows: 8 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/cl/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 3, rows: 8 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 3, rows: 4 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 3, rows: 4 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 3, rows: 8 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 3, rows: 8 }
];
} else {
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 3, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/cl/onchain', icon: this.faChartPie, title: 'Balances', cols: 3, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/cl/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 4, rows: 2 },
{ id: 'fee', goTo: 'Routing', link: '/cl/routing', icon: this.faBolt, title: 'Routing Fee', cols: 3, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/cl/connections', icon: this.faNetworkWired, title: 'Channels', cols: 3, rows: 1 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 3, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 3, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 4, rows: 2 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 3, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'connections/channels/pending'], icon: this.faNetworkWired, title: 'Channels', cols: 3, rows: 1 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/cl/onchain', icon: this.faChartPie, title: 'Balances', cols: 2, rows: 5 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/cl/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/cl/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'transactions', goTo: 'Transactions', link: '/cl/transactions', title: '', cols: 2, rows: 5 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 2, rows: 5 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 2, rows: 5 }
];
}
}
@ -189,7 +199,7 @@ export class CLHomeComponent implements OnInit, OnDestroy {
}
onNavigateTo(link: string) {
this.router.navigateByUrl(link);
this.router.navigateByUrl('/cl/' + link);
}
onsortChannelsBy() {

@ -1,11 +1,12 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { CommonService } from '../../../../../shared/services/common.service';
import { DataService } from '../../../../../shared/services/data.service';
import { LoggerService } from '../../../../../shared/services/logger.service';
import { mockCLEffects, mockDataService, mockLoggerService, mockECLEffects, mockLNDEffects, mockRTLEffects } from '../../../../../shared/test-helpers/mock-services';
import { mockCLEffects, mockDataService, mockLoggerService, mockECLEffects, mockLNDEffects, mockRTLEffects, mockRouter } from '../../../../../shared/test-helpers/mock-services';
import { SharedModule } from '../../../../../shared/shared.module';
import { RTLEffects } from '../../../../../store/rtl.effects';
@ -15,6 +16,7 @@ import { CLReducer } from '../../../../../clightning/store/cl.reducers';
import { ECLReducer } from '../../../../../eclair/store/ecl.reducers';
import { CLEffects } from '../../../../store/cl.effects';
import { CLChannelOpenTableComponent } from './channel-open-table.component';
import { ExtraOptions, Route, Router } from '@angular/router';
describe('CLChannelOpenTableComponent', () => {
let component: CLChannelOpenTableComponent;
@ -26,11 +28,13 @@ describe('CLChannelOpenTableComponent', () => {
imports: [
BrowserAnimationsModule,
SharedModule,
RouterTestingModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cl: CLReducer, ecl: ECLReducer }),
EffectsModule.forRoot([mockRTLEffects, mockLNDEffects, mockCLEffects, mockECLEffects])
],
providers: [
CommonService,
{ provide: Router, useClass: mockRouter },
{ provide: LoggerService, useClass: mockLoggerService },
{ provide: DataService, useClass: mockDataService },
{ provide: RTLEffects, useClass: mockRTLEffects },

@ -1,4 +1,5 @@
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@ -55,7 +56,7 @@ export class CLChannelOpenTableComponent implements OnInit, AfterViewInit, OnDes
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<RTLState>, private rtlEffects: RTLEffects, private clEffects: CLEffects, private commonService: CommonService) {
constructor(private logger: LoggerService, private store: Store<RTLState>, private rtlEffects: RTLEffects, private clEffects: CLEffects, private commonService: CommonService, private router: Router) {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
@ -70,6 +71,7 @@ export class CLChannelOpenTableComponent implements OnInit, AfterViewInit, OnDes
this.flgSticky = true;
this.displayedColumns = ['short_channel_id', 'alias', 'msatoshi_to_us', 'msatoshi_to_them', 'balancedness', 'actions'];
}
this.selFilter = this.router.getCurrentNavigation().extras?.state?.filter ? this.router.getCurrentNavigation().extras?.state?.filter : '';
}
ngOnInit() {

@ -9,22 +9,34 @@
<div *ngIf="errorMessage === ''" [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container">
<mat-progress-bar *ngIf="apiCallStatus?.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="failedForwardingEvents" fxFlex="100" matSort class="overflow-auto">
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Status</th>
<td mat-cell *matCellDef="let fhEvent">{{(fhEvent?.status === 'local_failed' ? 'local failed' : fhEvent?.status ) | titlecase}}</td>
</ng-container>
<ng-container matColumnDef="received_time">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Received Time</th>
<td mat-cell *matCellDef="let fhEvent">{{(fhEvent?.received_time * 1000) | date:'dd/MMM/y HH:mm'}}</td>
</ng-container>
<ng-container matColumnDef="resolved_time">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Resolved Time</th>
<td mat-cell *matCellDef="let fhEvent">{{(fhEvent?.resolved_time * 1000) | date:'dd/MMM/y HH:mm'}}</td>
</ng-container>
<ng-container matColumnDef="in_channel">
<th mat-header-cell *matHeaderCellDef mat-sort-header>In Channel</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent?.in_channel_alias}}</td>
</ng-container>
<ng-container matColumnDef="out_channel">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Out Channel</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent?.out_channel_alias}}</td>
</ng-container>
<ng-container matColumnDef="in_msatoshi">
<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?.in_msatoshi/1000 | number:fhEvent?.in_msatoshi < 1000 ? '1.0-4' : '1.0-0'}}</span></td>
</ng-container>
<ng-container matColumnDef="out_msatoshi">
<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?.out_msatoshi/1000 | number:fhEvent?.out_msatoshi < 1000 ? '1.0-4' : '1.0-0'}}</span></td>
</ng-container>
<ng-container matColumnDef="fee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Fee (mSats)</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent?.fee | number:'1.0-0'}}</span></td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3">
<div class="bordered-box table-actions-select">

@ -24,7 +24,7 @@ import { failedForwardingHistory } from '../../store/cl.selector';
templateUrl: './failed-transactions.component.html',
styleUrls: ['./failed-transactions.component.scss'],
providers: [
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Events') }
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Failed Events') }
]
})
export class CLFailedTransactionsComponent implements OnInit, AfterViewInit, OnDestroy {
@ -49,13 +49,13 @@ export class CLFailedTransactionsComponent implements OnInit, AfterViewInit, OnD
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
this.displayedColumns = ['status', 'received_time', 'in_msatoshi', 'actions'];
this.displayedColumns = ['received_time', 'in_channel', 'in_msatoshi', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.SM || this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['status', 'received_time', 'in_msatoshi', 'actions'];
this.displayedColumns = ['received_time', 'in_channel', 'out_channel', 'in_msatoshi', 'out_msatoshi', 'actions'];
} else {
this.flgSticky = true;
this.displayedColumns = ['status', 'received_time', 'in_channel', 'in_msatoshi', 'actions'];
this.displayedColumns = ['received_time', 'resolved_time', 'in_channel', 'out_channel', 'in_msatoshi', 'out_msatoshi', 'fee', 'actions'];
}
}
@ -86,17 +86,22 @@ export class CLFailedTransactionsComponent implements OnInit, AfterViewInit, OnD
onFailedEventClick(selFEvent: ForwardingEvent) {
const reorderedFHEvent = [
[{ key: 'payment_hash', value: selFEvent.payment_hash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING }],
[{ key: 'status', value: selFEvent.status === 'local_failed' ? 'Local Failed' : this.commonService.titleCase(selFEvent.status), title: 'Status', width: 50, type: DataTypeEnum.STRING },
{ key: 'received_time', value: selFEvent.received_time, title: 'Received Time', width: 50, type: DataTypeEnum.DATE_TIME }],
[{ key: 'in_channel', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING },
{ key: 'in_msatoshi', value: selFEvent.in_msatoshi, title: 'In (mSats)', width: 50, type: DataTypeEnum.NUMBER }]
[{ key: 'received_time', value: selFEvent.received_time, title: 'Received Time', width: 50, type: DataTypeEnum.DATE_TIME },
{ key: 'resolved_time', value: selFEvent.resolved_time, title: 'Resolved Time', width: 50, type: DataTypeEnum.DATE_TIME }],
[{ key: 'in_channel_alias', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING },
{ key: 'out_channel_alias', value: selFEvent.out_channel_alias, title: 'Outbound Channel', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'in_msatoshi', value: selFEvent.in_msatoshi, title: 'Amount In (mSats)', width: 33, type: DataTypeEnum.NUMBER },
{ key: 'out_msatoshi', value: selFEvent.out_msatoshi, title: 'Amount Out (mSats)', width: 33, type: DataTypeEnum.NUMBER },
{ key: 'fee', value: selFEvent.fee, title: 'Fee (mSats)', width: 34, type: DataTypeEnum.NUMBER }]
];
if (selFEvent.payment_hash) {
reorderedFHEvent.unshift([{ key: 'payment_hash', value: selFEvent.payment_hash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING }]);
}
this.store.dispatch(openAlert({
payload: {
data: {
type: AlertTypeEnum.INFORMATION,
alertTitle: 'Event Information',
alertTitle: 'Failed Event Information',
message: reorderedFHEvent
}
}
@ -109,11 +114,12 @@ export class CLFailedTransactionsComponent implements OnInit, AfterViewInit, OnD
this.failedForwardingEvents.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null);
this.failedForwardingEvents.paginator = this.paginator;
this.failedForwardingEvents.filterPredicate = (event: ForwardingEvent, fltr: string) => {
const newEvent = (event.status ? (event.status === 'local_failed') ? 'local failed' : event.status.toLowerCase() : '') +
(event.received_time ? this.datePipe.transform(new Date(event.received_time * 1000), 'dd/MMM/YYYY HH:mm').toLowerCase() : '') +
const newEvent = ((event.received_time ? this.datePipe.transform(new Date(event.received_time * 1000), 'dd/MMM/YYYY HH:mm').toLowerCase() : '') +
(event.resolved_time ? this.datePipe.transform(new Date(event.resolved_time * 1000), 'dd/MMM/YYYY HH:mm').toLowerCase() : '') +
(event.payment_hash ? event.payment_hash.toLowerCase() : '') +
(event.in_channel ? event.in_channel.toLowerCase() : '') + (event.out_channel ? event.out_channel.toLowerCase() : '') +
(event.in_msatoshi ? (event.in_msatoshi / 1000) : '') + (event.out_msatoshi ? (event.out_msatoshi / 1000) : '') + (event.fee ? event.fee : '');
(event.in_channel_alias ? event.in_channel_alias.toLowerCase() : '') + (event.out_channel_alias ? event.out_channel_alias.toLowerCase() : '') +
(event.in_msatoshi ? (event.in_msatoshi / 1000) : '') + (event.out_msatoshi ? (event.out_msatoshi / 1000) : '') + (event.fee ? event.fee : ''));
return newEvent.includes(fltr);
};
this.applyFilter();

@ -0,0 +1,54 @@
<div fxLayout="column" fxLayoutAlign="start stretch" class="padding-gap-x">
<div class="p-2 error-border my-2" *ngIf="errorMessage !== ''">{{errorMessage}}</div>
<div *ngIf="errorMessage === ''" fxLayout="column" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70"></div>
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter()" [(ngModel)]="selFilter" placeholder="Filter">
</mat-form-field>
</div>
<div *ngIf="errorMessage === ''" [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container">
<mat-progress-bar *ngIf="apiCallStatus?.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="failedLocalForwardingEvents" fxFlex="100" matSort class="overflow-auto">
<ng-container matColumnDef="received_time">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Received Time</th>
<td mat-cell *matCellDef="let fhEvent">{{(fhEvent?.received_time * 1000) | date:'dd/MMM/y HH:mm'}}</td>
</ng-container>
<ng-container matColumnDef="in_channel">
<th mat-header-cell *matHeaderCellDef mat-sort-header>In Channel</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent?.in_channel_alias}}</td>
</ng-container>
<ng-container matColumnDef="in_msatoshi">
<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?.in_msatoshi/1000 | number:fhEvent?.in_msatoshi < 1000 ? '1.0-4' : '1.0-0'}}</span></td>
</ng-container>
<ng-container matColumnDef="failreason">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="pl-3">Fail Reason</th>
<td mat-cell *matCellDef="let fhEvent" class="pl-3">{{CLFailReason[fhEvent?.failreason]}}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3">
<div class="bordered-box table-actions-select">
<mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger>
<mat-option (click)="onDownloadCSV()">Download CSV</mat-option>
</mat-select>
</div>
</th>
<td mat-cell *matCellDef="let fhEvent" class="px-3" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onFailedLocalEventClick(fhEvent)">View Info</button>
</td>
</ng-container>
<ng-container matColumnDef="no_event">
<td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="(!failedLocalForwardingEvents?.data || failedLocalForwardingEvents?.data?.length<1) && apiCallStatus?.status === apiCallStatusEnum.COMPLETED">No failed transaction available.</p>
<p *ngIf="(!failedLocalForwardingEvents?.data || failedLocalForwardingEvents?.data?.length<1) && apiCallStatus?.status === apiCallStatusEnum.INITIATED">Getting failed transactions...</p>
<p *ngIf="(!failedLocalForwardingEvents?.data || failedLocalForwardingEvents?.data?.length<1) && apiCallStatus?.status === apiCallStatusEnum.ERROR">{{errorMessage}}</p>
</td>
</ng-container>
<tr mat-footer-row *matFooterRowDef="['no_event']" [ngClass]="{'display-none': failedLocalForwardingEvents?.data && failedLocalForwardingEvents?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<mat-paginator *ngIf="errorMessage === ''" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" [showFirstLastButtons]="screenSize === screenSizeEnum.XS ? false : true" class="mb-1"></mat-paginator>
</div>

@ -0,0 +1,53 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule } from '@ngrx/store';
import { RootReducer } from '../../../store/rtl.reducers';
import { LNDReducer } from '../../../lnd/store/lnd.reducers';
import { CLReducer } from '../../store/cl.reducers';
import { ECLReducer } from '../../../eclair/store/ecl.reducers';
import { CommonService } from '../../../shared/services/common.service';
import { LoggerService } from '../../../shared/services/logger.service';
import { CLLocalFailedTransactionsComponent } from './local-failed-transactions.component';
import { mockDataService, mockLoggerService } from '../../../shared/test-helpers/mock-services';
import { SharedModule } from '../../../shared/shared.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DataService } from '../../../shared/services/data.service';
describe('CLLocalFailedTransactionsComponent', () => {
let component: CLLocalFailedTransactionsComponent;
let fixture: ComponentFixture<CLLocalFailedTransactionsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [CLLocalFailedTransactionsComponent],
imports: [
BrowserAnimationsModule,
RouterTestingModule,
SharedModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cl: CLReducer, ecl: ECLReducer })
],
providers: [
CommonService,
{ provide: LoggerService, useClass: mockLoggerService },
{ provide: DataService, useClass: mockDataService }
]
}).
compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLLocalFailedTransactionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
afterEach(() => {
TestBed.resetTestingModule();
});
});

@ -0,0 +1,147 @@
import { Component, OnInit, ViewChild, OnDestroy, AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common';
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 { LocalFailedEvent } from '../../../shared/models/clModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, APICallStatusEnum, CLFailReason } from '../../../shared/services/consts-enums-functions';
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
import { LoggerService } from '../../../shared/services/logger.service';
import { CommonService } from '../../../shared/services/common.service';
import { RTLState } from '../../../store/rtl.state';
import { openAlert } from '../../../store/rtl.actions';
import { getLocalFailedForwardingHistory } from '../../store/cl.actions';
import { localFailedForwardingHistory } from '../../store/cl.selector';
@Component({
selector: 'rtl-cl-local-failed-history',
templateUrl: './local-failed-transactions.component.html',
styleUrls: ['./local-failed-transactions.component.scss'],
providers: [
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Local Failed Events') }
]
})
export class CLLocalFailedTransactionsComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | undefined;
public CLFailReason = CLFailReason;
public failedLocalEvents: any;
public errorMessage = '';
public displayedColumns: any[] = [];
public failedLocalForwardingEvents: any;
public flgSticky = false;
public selFilter = '';
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
public apiCallStatus: ApiCallStatusPayload = null;
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private datePipe: DatePipe, private router: Router) {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
this.displayedColumns = ['received_time', 'in_channel', 'in_msatoshi', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.SM || this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['received_time', 'in_channel', 'in_msatoshi', 'actions'];
} else {
this.flgSticky = true;
this.displayedColumns = ['received_time', 'in_channel', 'in_msatoshi', 'failreason', 'actions'];
}
}
ngOnInit() {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.store.dispatch(getLocalFailedForwardingHistory());
this.store.select(localFailedForwardingHistory).pipe(takeUntil(this.unSubs[0])).
subscribe((lffhSeletor: { localFailedForwardingHistory: LocalFailedEvent[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = lffhSeletor.apiCallStatus;
if (this.apiCallStatus.status === APICallStatusEnum.ERROR) {
this.errorMessage = (typeof (this.apiCallStatus.message) === 'object') ? JSON.stringify(this.apiCallStatus.message) : this.apiCallStatus.message;
}
this.failedLocalEvents = lffhSeletor.localFailedForwardingHistory || [];
if (this.failedLocalEvents.length > 0 && this.sort && this.paginator) {
this.loadLocalfailedLocalEventsTable(this.failedLocalEvents);
}
this.logger.info(lffhSeletor);
});
}
ngAfterViewInit() {
if (this.failedLocalEvents.length > 0) {
this.loadLocalfailedLocalEventsTable(this.failedLocalEvents);
}
}
onFailedLocalEventClick(selFEvent: LocalFailedEvent) {
const reorderedFHEvent = [
[{ key: 'received_time', value: selFEvent.received_time, title: 'Received Time', width: 50, type: DataTypeEnum.DATE_TIME },
{ key: 'in_channel_alias', value: selFEvent.in_channel_alias, title: 'Inbound Channel', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'in_msatoshi', value: selFEvent.in_msatoshi, title: 'Amount In (mSats)', width: 100, type: DataTypeEnum.NUMBER }],
[{ key: 'failreason', value: this.CLFailReason[selFEvent.failreason], title: 'Reason for Failure', width: 100, type: DataTypeEnum.STRING }]
];
this.store.dispatch(openAlert({
payload: {
data: {
type: AlertTypeEnum.INFORMATION,
alertTitle: 'Local Failed Event Information',
message: reorderedFHEvent
}
}
}));
}
loadLocalfailedLocalEventsTable(forwardingEvents: LocalFailedEvent[]) {
this.failedLocalForwardingEvents = new MatTableDataSource<LocalFailedEvent>([...forwardingEvents]);
this.failedLocalForwardingEvents.filterPredicate = (event: LocalFailedEvent, fltr: string) => {
const newEvent = ((event.received_time ? this.datePipe.transform(new Date(event.received_time * 1000), 'dd/MMM/YYYY HH:mm').toLowerCase() : '') +
(event.in_channel_alias ? event.in_channel_alias.toLowerCase() : '') +
((event.failreason && this.CLFailReason[event.failreason]) ? this.CLFailReason[event.failreason].toLowerCase() : '') +
(event.in_msatoshi ? (event.in_msatoshi / 1000) : ''));
return newEvent.includes(fltr);
};
this.failedLocalForwardingEvents.sort = this.sort;
this.failedLocalForwardingEvents.sortingDataAccessor = (data: LocalFailedEvent, sortHeaderId: string) => {
switch (sortHeaderId) {
case 'failreason':
return this.CLFailReason[data.failreason];
default:
return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null;
}
};
this.failedLocalForwardingEvents.paginator = this.paginator;
this.applyFilter();
this.logger.info(this.failedLocalForwardingEvents);
}
onDownloadCSV() {
if (this.failedLocalForwardingEvents && this.failedLocalForwardingEvents.data && this.failedLocalForwardingEvents.data.length > 0) {
this.commonService.downloadFile(this.failedLocalForwardingEvents.data, 'Local-failed-transactions');
}
}
applyFilter() {
this.failedLocalForwardingEvents.filter = this.selFilter.trim().toLowerCase();
}
ngOnDestroy() {
this.unSubs.forEach((completeSub) => {
completeSub.next(null);
completeSub.complete();
});
}
}

@ -12,11 +12,11 @@ import { faMapSigns } from '@fortawesome/free-solid-svg-icons';
export class CLRoutingComponent implements OnInit, OnDestroy {
public faMapSigns = faMapSigns;
public links = [{ link: 'forwardinghistory', name: 'Forwarding History' }, { link: 'routingpeers', name: 'Routing Peers' }, { link: 'failedtransactions', name: 'Failed Transactions' }];
public links = [{ link: 'forwardinghistory', name: 'Forwarding History' }, { link: 'routingpeers', name: 'Routing Peers' }, { link: 'failedtransactions', name: 'Failed Transactions' }, { link: 'localfail', name: 'Local Failed Transactions' }];
public activeLink = this.links[0].link;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private router: Router) {}
constructor(private router: Router) { }
ngOnInit() {
const linkFound = this.links.find((link) => this.router.url.includes(link.link));

@ -3,7 +3,7 @@ import { createAction, props } from '@ngrx/store';
import { CLActions } from '../../shared/services/consts-enums-functions';
import { ApiCallStatusPayload } from '../../shared/models/apiCallsPayload';
import { SelNodeChild } from '../../shared/models/RTLconfig';
import { GetInfo, Fees, Peer, Payment, PayRequest, QueryRoutes, Channel, FeeRates, ForwardingEvent, Invoice, ListInvoices, OnChain, UTXO, SaveChannel, GetNewAddress, DetachPeer, UpdateChannel, CloseChannel, DecodePayment, SendPayment, GetQueryRoutes, ChannelLookup, OfferInvoice, Offer, OfferBookmark } from '../../shared/models/clModels';
import { GetInfo, Fees, Peer, Payment, QueryRoutes, Channel, FeeRates, ForwardingEvent, Invoice, ListInvoices, OnChain, UTXO, SaveChannel, GetNewAddress, DetachPeer, UpdateChannel, CloseChannel, SendPayment, GetQueryRoutes, ChannelLookup, OfferInvoice, Offer, OfferBookmark, LocalFailedEvent } from '../../shared/models/clModels';
export const updateCLAPICallStatus = createAction(CLActions.UPDATE_API_CALL_STATUS_CL, props<{ payload: ApiCallStatusPayload }>());
@ -89,6 +89,10 @@ export const getFailedForwardingHistory = createAction(CLActions.GET_FAILED_FORW
export const setFailedForwardingHistory = createAction(CLActions.SET_FAILED_FORWARDING_HISTORY_CL, props<{ payload: ForwardingEvent[] }>());
export const getLocalFailedForwardingHistory = createAction(CLActions.GET_LOCAL_FAILED_FORWARDING_HISTORY_CL);
export const setLocalFailedForwardingHistory = createAction(CLActions.SET_LOCAL_FAILED_FORWARDING_HISTORY_CL, props<{ payload: LocalFailedEvent[] }>());
export const fetchInvoices = createAction(CLActions.FETCH_INVOICES_CL, props<{ payload: { num_max_invoices?: number, index_offset?: number, reversed?: boolean } }>());
export const setInvoices = createAction(CLActions.SET_INVOICES_CL, props<{ payload: ListInvoices }>());

@ -663,17 +663,25 @@ export class CLEffects implements OnDestroy {
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchForwardingHistory', status: APICallStatusEnum.COMPLETED } }));
const isNewerVersion = (nodeInfo.api_version) ? this.commonService.isVersionCompatible(nodeInfo.api_version, '0.5.0') : false;
if (!isNewerVersion) {
const filteredLocalFailedEvents = [];
const filteredFailedEvents = [];
const filteredSuccesfulEvents = [];
fhRes.forEach((event: ForwardingEvent) => {
if (event.status === 'settled') {
filteredSuccesfulEvents.push(event);
} else if (event.status === 'failed' || event.status === 'local_failed') {
} else if (event.status === 'failed') {
filteredFailedEvents.push(event);
} else if (event.status === 'local_failed') {
filteredLocalFailedEvents.push(event);
}
});
fhRes = JSON.parse(JSON.stringify(filteredSuccesfulEvents));
this.store.dispatch(setFailedForwardingHistory({ payload: filteredFailedEvents }));
if (action.payload.status === 'failed') {
this.store.dispatch(setFailedForwardingHistory({ payload: filteredFailedEvents }));
}
if (action.payload.status === 'local_failed') {
this.store.dispatch(setFailedForwardingHistory({ payload: filteredLocalFailedEvents }));
}
}
return {
type: CLActions.SET_FORWARDING_HISTORY_CL,
@ -699,21 +707,44 @@ export class CLEffects implements OnDestroy {
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchFailedForwardingHistory', status: APICallStatusEnum.COMPLETED } }));
return of({ type: RTLActions.VOID });
} // For backwards compatibility < 0.5.0 END
let failedEventsReq = new Observable();
const failedRes = this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=failed');
const localFailedRes = this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=local_failed');
failedEventsReq = forkJoin([failedRes, localFailedRes]);
return failedEventsReq.pipe(map((ffhRes: any) => {
this.logger.info(ffhRes);
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchFailedForwardingHistory', status: APICallStatusEnum.COMPLETED } }));
return {
type: CLActions.SET_FAILED_FORWARDING_HISTORY_CL,
payload: this.commonService.sortDescByKey([...ffhRes[0], ...ffhRes[1]], 'received_time')
};
}), catchError((err) => {
this.handleErrorWithAlert('FetchFailedForwardingHistory', UI_MESSAGES.NO_SPINNER, 'Get Failed Forwarding History Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=failed', err);
return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=failed').
pipe(map((ffhRes: any) => {
this.logger.info(ffhRes);
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchFailedForwardingHistory', status: APICallStatusEnum.COMPLETED } }));
return {
type: CLActions.SET_FAILED_FORWARDING_HISTORY_CL,
payload: ffhRes
};
}), catchError((err) => {
this.handleErrorWithAlert('FetchFailedForwardingHistory', UI_MESSAGES.NO_SPINNER, 'Get Failed Forwarding History Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=failed', err);
return of({ type: RTLActions.VOID });
}));
}))
);
fetchLocalFailedForwardingHistoryCL = createEffect(() => this.actions.pipe(
ofType(CLActions.GET_LOCAL_FAILED_FORWARDING_HISTORY_CL),
withLatestFrom(this.store.select(clNodeInformation)),
mergeMap(([action, nodeInfo]: [{ type: string, payload: any }, GetInfo]) => {
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchLocalFailedForwardingHistory', status: APICallStatusEnum.INITIATED } }));
// For backwards compatibility < 0.5.0 START
const isNewerVersion = (nodeInfo.api_version) ? this.commonService.isVersionCompatible(nodeInfo.api_version, '0.5.0') : false;
if (!isNewerVersion) {
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchLocalFailedForwardingHistory', status: APICallStatusEnum.COMPLETED } }));
return of({ type: RTLActions.VOID });
}));
} // For backwards compatibility < 0.5.0 END
return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=local_failed').
pipe(map((lffhRes: any) => {
this.logger.info(lffhRes);
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchLocalFailedForwardingHistory', status: APICallStatusEnum.COMPLETED } }));
return {
type: CLActions.SET_LOCAL_FAILED_FORWARDING_HISTORY_CL,
payload: lffhRes
};
}), catchError((err) => {
this.handleErrorWithAlert('FetchLocalFailedForwardingHistory', UI_MESSAGES.NO_SPINNER, 'Get Local Failed Forwarding History Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/listForwards?status=local_failed', err);
return of({ type: RTLActions.VOID });
}));
}))
);

@ -2,7 +2,7 @@ import { createReducer, on } from '@ngrx/store';
import { initCLState } from './cl.state';
import {
addInvoice, addPeer, removeChannel, removePeer, resetCLStore, setBalance, setChannels,
setChildNodeSettingsCL, setFailedForwardingHistory, setFeeRates, setFees, setForwardingHistory,
setChildNodeSettingsCL, setFailedForwardingHistory, setLocalFailedForwardingHistory, setFeeRates, setFees, setForwardingHistory,
setInfo, setInvoices, setLocalRemoteBalance, setOffers, addOffer, setPayments, setPeers, setUTXOs,
updateCLAPICallStatus, updateInvoice, updateOffer, setOfferBookmarks, addUpdateOfferBookmark, removeOfferBookmark
} from './cl.actions';
@ -125,6 +125,14 @@ export const CLReducer = createReducer(initCLState,
failedForwardingHistory: payload
};
}),
on(setLocalFailedForwardingHistory, (state, { payload }) => {
const storedChannels = [...state.activeChannels, ...state.pendingChannels, ...state.inactiveChannels];
payload = mapAliases(payload, storedChannels);
return {
...state,
localFailedForwardingHistory: payload
};
}),
on(addInvoice, (state, { payload }) => {
const newInvoices = state.invoices;
newInvoices.invoices.unshift(payload);
@ -222,13 +230,13 @@ const mapAliases = (payload: any, storedChannels: Channel[]) => {
if (fhEvent.in_channel_alias) { return; }
}
if (idx === storedChannels.length - 1) {
if (!fhEvent.in_channel_alias) { fhEvent.in_channel_alias = fhEvent.in_channel; }
if (!fhEvent.out_channel_alias) { fhEvent.out_channel_alias = fhEvent.out_channel; }
if (!fhEvent.in_channel_alias) { fhEvent.in_channel_alias = fhEvent.in_channel ? fhEvent.in_channel : '-'; }
if (!fhEvent.out_channel_alias) { fhEvent.out_channel_alias = fhEvent.out_channel ? fhEvent.out_channel : '-'; }
}
}
} else {
fhEvent.in_channel_alias = fhEvent.in_channel;
fhEvent.out_channel_alias = fhEvent.out_channel;
fhEvent.in_channel_alias = fhEvent.in_channel ? fhEvent.in_channel : '-';
fhEvent.out_channel_alias = fhEvent.out_channel ? fhEvent.out_channel : '-';
}
});
} else {

@ -1,6 +1,4 @@
import { createFeatureSelector, createSelector, select } from '@ngrx/store';
import { pipe, filter, map, take, find } from 'rxjs';
import { ApiCallStatusPayload } from '../../shared/models/apiCallsPayload';
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { Offer } from '../../shared/models/clModels';
import { CLState } from './cl.state';
@ -21,6 +19,7 @@ export const balance = createSelector(clState, (state: CLState) => ({ balance: s
export const localRemoteBalance = createSelector(clState, (state: CLState) => ({ localRemoteBalance: state.localRemoteBalance, apiCallStatus: state.apisCallStatus.FetchLocalRemoteBalance }));
export const forwardingHistory = createSelector(clState, (state: CLState) => ({ forwardingHistory: state.forwardingHistory, apiCallStatus: state.apisCallStatus.FetchForwardingHistory }));
export const failedForwardingHistory = createSelector(clState, (state: CLState) => ({ failedForwardingHistory: state.failedForwardingHistory, apiCallStatus: state.apisCallStatus.FetchFailedForwardingHistory }));
export const localFailedForwardingHistory = createSelector(clState, (state: CLState) => ({ localFailedForwardingHistory: state.localFailedForwardingHistory, apiCallStatus: state.apisCallStatus.FetchLocalFailedForwardingHistory }));
export const nodeInfoAndNodeSettingsAndBalance = createSelector(clState, (state: CLState) => ({ information: state.information, nodeSettings: state.nodeSettings, balance: state.balance }));
export const nodeInfoAndBalanceAndNumPeers = createSelector(clState, (state: CLState) => ({ information: state.information, balance: state.balance, numPeers: state.peers.length }));
export const nodeInfoAndBalance = createSelector(clState, (state: CLState) => ({ information: state.information, balance: state.balance }));

@ -1,6 +1,6 @@
import { SelNodeChild } from '../../shared/models/RTLconfig';
import { APICallStatusEnum, UserPersonaEnum } from '../../shared/services/consts-enums-functions';
import { GetInfo, Fees, Balance, LocalRemoteBalance, Peer, Payment, Channel, FeeRates, ForwardingEvent, ListInvoices, UTXO, Offer, OfferBookmark } from '../../shared/models/clModels';
import { GetInfo, Fees, Balance, LocalRemoteBalance, Peer, Payment, Channel, FeeRates, ForwardingEvent, ListInvoices, UTXO, Offer, OfferBookmark, LocalFailedEvent } from '../../shared/models/clModels';
import { ApiCallsListCL } from '../../shared/models/apiCallsPayload';
export interface CLState {
@ -19,6 +19,7 @@ export interface CLState {
payments: Payment[];
forwardingHistory: ForwardingEvent[];
failedForwardingHistory: ForwardingEvent[];
localFailedForwardingHistory: LocalFailedEvent[];
invoices: ListInvoices;
utxos: UTXO[];
offers: Offer[];
@ -40,6 +41,7 @@ export const initCLState: CLState = {
FetchPayments: { status: APICallStatusEnum.UN_INITIATED },
FetchForwardingHistory: { status: APICallStatusEnum.UN_INITIATED },
FetchFailedForwardingHistory: { status: APICallStatusEnum.UN_INITIATED },
FetchLocalFailedForwardingHistory: { status: APICallStatusEnum.UN_INITIATED },
FetchOffers: { status: APICallStatusEnum.UN_INITIATED },
FetchOfferBookmarks: { status: APICallStatusEnum.UN_INITIATED }
},
@ -57,6 +59,7 @@ export const initCLState: CLState = {
payments: [],
forwardingHistory: [],
failedForwardingHistory: [],
localFailedForwardingHistory: [],
invoices: { invoices: [] },
utxos: [],
offers: [],

@ -15,7 +15,9 @@
<div class="channels-capacity-scroll" [perfectScrollbar]>
<div fxLayout="column" fxFlex="100" *ngIf="allChannels && allChannels?.length > 0; else noChannelBlock">
<div *ngFor="let channel of allChannels" class="mt-2">
<span class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.shortChannelId}}" matTooltipDisabled="{{(channel.alias || channel.shortChannelId).length < 26}}">{{(channel?.alias || channel?.shortChannelId) | slice:0:24}}{{(channel?.alias || channel?.shortChannelId).length > 25 ? '...' : ''}}</span>
<a [routerLink]="['../connections/channels/open']" [state]="{filter: channel.channelId}" class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.shortChannelId}}" matTooltipDisabled="{{(channel.alias || channel.shortChannelId).length < 26}}">
{{(channel?.alias || channel?.shortChannelId) | slice:0:24}}{{(channel?.alias || channel?.shortChannelId).length > 25 ? '...' : ''}}
</a>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint fxFlex="40" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Local:</strong>{{channel?.toLocal || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint fxFlex="20" fxLayoutAlign="center center" class="font-size-90 color-primary">

@ -8,7 +8,9 @@
<div [perfectScrollbar] fxLayout="column" fxFlex.gt-sm="88" fxFlex="84" fxLayoutAlign="start start">
<div fxLayout="column" fxFlex="100" class="w-100" *ngIf="allChannels && allChannels.length > 0; else noChannelBlock">
<div *ngFor="let channel of allChannels" class="mt-2">
<span class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.shortChannelId}}" matTooltipDisabled="{{(channel.alias || channel.shortChannelId).length < 26}}">{{(channel.alias || channel.shortChannelId) | slice:0:24}}{{(channel.alias || channel.shortChannelId).length > 25 ? '...' : ''}}</span>
<a [routerLink]="['../connections/channels/open']" [state]="{filter: channel.channelId}" class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.shortChannelId}}" matTooltipDisabled="{{(channel.alias || channel.shortChannelId).length < 26}}">
{{(channel.alias || channel.shortChannelId) | slice:0:24}}{{(channel.alias || channel.shortChannelId).length > 25 ? '...' : ''}}
</a>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint *ngIf="direction === 'In'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.toRemote || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint *ngIf="direction === 'Out'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.toLocal || 0 | number:'1.0-0'}} Sats</mat-hint>

@ -13,13 +13,12 @@
<span>{{card.title}}</span>
</div>
<div>
<button *ngIf="card.link" mat-icon-button class="more-button" [matMenuTriggerFor]="menuOperator"
<button *ngIf="card.links[0]" mat-icon-button class="more-button" [matMenuTriggerFor]="menuOperator"
aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuOperator="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button *ngIf="card.id === 'fee'" (click)="onNavigateTo('/ecl/reports')" mat-menu-item>Fees Summary</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
<button *ngIf="card.id === 'capacity'" (click)="onsortChannelsBy()" mat-menu-item>Sort By
{{sortField === 'Balance Score' ? 'Capacity' : 'Balance Score'}}</button>
</mat-menu>
@ -66,12 +65,12 @@
<span>{{card.title}}</span>
</div>
<div>
<button *ngIf="card.link" mat-icon-button class="more-button" [matMenuTriggerFor]="menuMerchant"
<button *ngIf="card.links[0]" mat-icon-button class="more-button" [matMenuTriggerFor]="menuMerchant"
aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuMerchant="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
</mat-menu>
</div>
</mat-card-title>
@ -105,8 +104,7 @@
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuTransactions="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button mat-menu-item (click)="onNavigateTo('/ecl/reports/transactions')">Transactions Summary</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
</mat-menu>
</ng-template>
</mat-tab>

@ -16,6 +16,16 @@ import { SelNodeChild } from '../../shared/models/RTLconfig';
import { RTLState } from '../../store/rtl.state';
import { allChannelsInfo, eclNodeSettings, fees, nodeInfoStatus, onchainBalance } from '../store/ecl.selector';
export interface Tile {
id: string;
title: string;
cols: number;
rows: number;
goToOptions?: string[];
links?: string[];
icon?: any;
}
@Component({
selector: 'rtl-ecl-home',
templateUrl: './home.component.html',
@ -45,8 +55,8 @@ export class ECLHomeComponent implements OnInit, OnDestroy {
public allOutboundChannels: Channel[] = [];
public totalInboundLiquidity = 0;
public totalOutboundLiquidity = 0;
public operatorCards = [];
public merchantCards = [];
public operatorCards: Tile[] = [];
public merchantCards: Tile[] = [];
public screenSize = '';
public operatorCardHeight = '330px';
public merchantCardHeight = '65px';
@ -63,45 +73,45 @@ export class ECLHomeComponent implements OnInit, OnDestroy {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 10, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/ecl/onchain', icon: this.faChartPie, title: 'Balances', cols: 10, rows: 1 },
{ id: 'fee', goTo: 'Routing', link: '/ecl/routing', icon: this.faBolt, title: 'Routing Fee', cols: 10, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/ecl/connections', icon: this.faNetworkWired, title: 'Channels', cols: 10, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/ecl/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 10, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 10, rows: 1 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 10, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'connections/channels/inactive'], icon: this.faNetworkWired, title: 'Channels', cols: 10, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/ecl/onchain', icon: this.faChartPie, title: 'Balances', cols: 6, rows: 4 },
{ id: 'transactions', goTo: 'Transactions', link: '/ecl/transactions', title: '', cols: 6, rows: 4 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/ecl/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 6, rows: 8 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/ecl/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 6, rows: 8 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 6, rows: 4 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 6, rows: 4 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 6, rows: 8 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 6, rows: 8 }
];
} else if (this.screenSize === ScreenSizeEnum.SM || this.screenSize === ScreenSizeEnum.MD) {
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 5, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/ecl/onchain', icon: this.faChartPie, title: 'Balances', cols: 5, rows: 1 },
{ id: 'fee', goTo: 'Routing', link: '/ecl/routing', icon: this.faBolt, title: 'Routing Fee', cols: 5, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/ecl/connections', icon: this.faNetworkWired, title: 'Channels', cols: 5, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/ecl/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 5, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 5, rows: 1 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 5, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'connections/channels/inactive'], icon: this.faNetworkWired, title: 'Channels', cols: 5, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/ecl/onchain', icon: this.faChartPie, title: 'Balances', cols: 3, rows: 4 },
{ id: 'transactions', goTo: 'Transactions', link: '/ecl/transactions', title: '', cols: 3, rows: 4 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/ecl/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 3, rows: 8 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/ecl/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 3, rows: 8 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 3, rows: 4 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 3, rows: 4 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 3, rows: 8 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 3, rows: 8 }
];
} else {
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 3, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/ecl/onchain', icon: this.faChartPie, title: 'Balances', cols: 3, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/ecl/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 4, rows: 2 },
{ id: 'fee', goTo: 'Routing', link: '/ecl/routing', icon: this.faBolt, title: 'Routing Fee', cols: 3, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/ecl/connections', icon: this.faNetworkWired, title: 'Channels', cols: 3, rows: 1 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 3, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 3, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 4, rows: 2 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 3, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'connections/channels/inactive'], icon: this.faNetworkWired, title: 'Channels', cols: 3, rows: 1 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/ecl/onchain', icon: this.faChartPie, title: 'Balances', cols: 2, rows: 5 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/ecl/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/ecl/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'transactions', goTo: 'Transactions', link: '/ecl/transactions', title: '', cols: 2, rows: 5 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 2, rows: 5 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 2, rows: 5 }
];
}
}
@ -168,7 +178,7 @@ export class ECLHomeComponent implements OnInit, OnDestroy {
}
onNavigateTo(link: string) {
this.router.navigateByUrl(link);
this.router.navigateByUrl('/ecl/' + link);
}
onsortChannelsBy() {

@ -1,4 +1,6 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule } from '@ngrx/store';
import { RootReducer } from '../../../../../store/rtl.reducers';
@ -9,11 +11,11 @@ import { CommonService } from '../../../../../shared/services/common.service';
import { LoggerService } from '../../../../../shared/services/logger.service';
import { ECLChannelOpenTableComponent } from './channel-open-table.component';
import { mockDataService, mockLoggerService, mockRTLEffects } from '../../../../../shared/test-helpers/mock-services';
import { mockDataService, mockLoggerService, mockRouter, mockRTLEffects } from '../../../../../shared/test-helpers/mock-services';
import { RTLEffects } from '../../../../../store/rtl.effects';
import { SharedModule } from '../../../../../shared/shared.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DataService } from '../../../../../shared/services/data.service';
import { Router } from '@angular/router';
describe('ECLChannelOpenTableComponent', () => {
let component: ECLChannelOpenTableComponent;
@ -25,10 +27,12 @@ describe('ECLChannelOpenTableComponent', () => {
imports: [
BrowserAnimationsModule,
SharedModule,
RouterTestingModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cl: CLReducer, ecl: ECLReducer })
],
providers: [
CommonService,
{ provide: Router, useClass: mockRouter },
{ provide: LoggerService, useClass: mockLoggerService },
{ provide: DataService, useClass: mockDataService },
{ provide: RTLEffects, useClass: mockRTLEffects }

@ -1,4 +1,5 @@
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@ -53,7 +54,7 @@ export class ECLChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<RTLState>, private rtlEffects: RTLEffects, private commonService: CommonService) {
constructor(private logger: LoggerService, private store: Store<RTLState>, private rtlEffects: RTLEffects, private commonService: CommonService, private router: Router) {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
@ -68,6 +69,7 @@ export class ECLChannelOpenTableComponent implements OnInit, AfterViewInit, OnDe
this.flgSticky = true;
this.displayedColumns = ['shortChannelId', 'alias', 'feeBaseMsat', 'feeProportionalMillionths', 'toLocal', 'toRemote', 'balancedness', 'actions'];
}
this.selFilter = this.router.getCurrentNavigation().extras?.state?.filter ? this.router.getCurrentNavigation().extras?.state?.filter : '';
}
ngOnInit() {

@ -15,7 +15,9 @@
<div class="channels-capacity-scroll" [perfectScrollbar]>
<div fxLayout="column" fxFlex="100" *ngIf="allChannels && allChannels.length > 0; else noChannelBlock">
<div *ngFor="let channel of allChannels" class="mt-2">
<span class="dashboard-capacity-header" matTooltip="{{channel.remote_alias || channel.remote_pubkey}}" matTooltipDisabled="{{(channel.remote_alias || channel.remote_pubkey).length < 26}}">{{(channel.remote_alias || channel.remote_pubkey) | slice:0:24}}{{(channel.remote_alias || channel.remote_pubkey).length > 25 ? '...' : ''}}</span>
<a [routerLink]="['../connections/channels/open']" [state]="{filter: channel.chan_id}" class="dashboard-capacity-header" matTooltip="{{channel.remote_alias || channel.remote_pubkey}}" matTooltipDisabled="{{(channel.remote_alias || channel.remote_pubkey).length < 26}}">
{{(channel.remote_alias || channel.remote_pubkey) | slice:0:24}}{{(channel.remote_alias || channel.remote_pubkey).length > 25 ? '...' : ''}}
</a>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint fxFlex="40" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Local:</strong>{{channel.local_balance || 0 | number}} Sats</mat-hint>
<mat-hint fxFlex="20" fxLayoutAlign="center center" class="font-size-90 color-primary">

@ -7,7 +7,9 @@
<div fxLayout="column" fxFlex="3" fxLayoutAlign="end stretch"><mat-divider class="dashboard-divider"></mat-divider></div>
<div [perfectScrollbar] *ngIf="allChannels && allChannels.length > 0; else noChannelBlock">
<div *ngFor="let channel of allChannels" fxLayout="column">
<span class="dashboard-capacity-header mt-2" matTooltip="{{channel.remote_alias || channel.remote_pubkey}}" matTooltipDisabled="{{(channel.remote_alias || channel.remote_pubkey).length < 26}}">{{(channel.remote_alias || channel.remote_pubkey) | slice:0:24}}{{(channel.remote_alias || channel.remote_pubkey).length > 25 ? '...' : ''}}</span>
<a [routerLink]="['../connections/channels/open']" [state]="{filter: channel.chan_id}" class="dashboard-capacity-header mt-2" matTooltip="{{channel.remote_alias || channel.remote_pubkey}}" matTooltipDisabled="{{(channel.remote_alias || channel.remote_pubkey).length < 26}}">
{{(channel.remote_alias || channel.remote_pubkey) | slice:0:24}}{{(channel.remote_alias || channel.remote_pubkey).length > 25 ? '...' : ''}}
</a>
<div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between start">
<mat-hint *ngIf="direction === 'In'" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.remote_balance || 0 | number}} Sats</mat-hint>
<div *ngIf="direction === 'Out'" fxLayout="row" fxFlex="100" fxLayoutAlign="start center">

@ -13,13 +13,12 @@
<span>{{card.title}}</span>
</div>
<div>
<button *ngIf="card.link" mat-icon-button class="more-button" [matMenuTriggerFor]="menuOperator" aria-label="Toggle menu">
<button *ngIf="card.links[0]" mat-icon-button class="more-button" [matMenuTriggerFor]="menuOperator" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuOperator="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button *ngIf="card.id === 'fee'" (click)="onNavigateTo('/lnd/reports')" mat-menu-item>Fees Summary</button>
<button *ngIf="card.id === 'capacity'" (click)="onsortChannelsBy()" mat-menu-item>Sort By {{sortField === 'Balance Score' ? 'Capacity' : 'Balance Score'}}</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
<button mat-menu-item *ngIf="card.id === 'capacity'" (click)="onsortChannelsBy()">Sort By {{sortField === 'Balance Score' ? 'Capacity' : 'Balance Score'}}</button>
</mat-menu>
</div>
</mat-card-title>
@ -66,11 +65,11 @@
<span>{{card.title}}</span>
</div>
<div>
<button *ngIf="card.link" mat-icon-button class="more-button" [matMenuTriggerFor]="menuMerchant" aria-label="Toggle menu">
<button *ngIf="card.links[0]" mat-icon-button class="more-button" [matMenuTriggerFor]="menuMerchant" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuMerchant="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
</mat-menu>
</div>
</mat-card-title>
@ -100,8 +99,7 @@
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuTransactions="matMenu" class="dashboard-vert-menu" xPosition="before">
<button mat-menu-item (click)="onNavigateTo(card.link)">Go To {{card.goTo}}</button>
<button mat-menu-item (click)="onNavigateTo('/lnd/reports/transactions')">Transactions Summary</button>
<button mat-menu-item *ngFor="let goToOption of card.goToOptions; index as i" (click)="onNavigateTo(card.links[i])">{{goToOption}}</button>
</mat-menu>
</ng-template>
</mat-tab>

@ -22,8 +22,8 @@ export interface Tile {
title: string;
cols: number;
rows: number;
goTo?: string;
link?: string;
goToOptions?: string[];
links?: string[];
icon?: any;
}
@ -79,65 +79,65 @@ export class HomeComponent implements OnInit, OnDestroy {
switch (this.screenSize) {
case ScreenSizeEnum.XS:
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 10, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/lnd/onchain', icon: this.faChartPie, title: 'Balances', cols: 10, rows: 1 },
{ id: 'fee', goTo: 'Routing', link: '/lnd/routing', icon: this.faBolt, title: 'Routing Fee', cols: 10, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/lnd/connections', icon: this.faNetworkWired, title: 'Channels', cols: 10, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/lnd/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 10, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 10, rows: 1 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 10, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'inactive'], icon: this.faNetworkWired, title: 'Channels', cols: 10, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/lnd/onchain', icon: this.faChartPie, title: 'Balances', cols: 6, rows: 4 },
{ id: 'transactions', goTo: 'Transactions', link: '/lnd/transactions', title: '', cols: 6, rows: 6 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/lnd/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 6, rows: 8 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/lnd/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 6, rows: 8 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 6, rows: 4 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 6, rows: 6 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 6, rows: 8 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 6, rows: 8 }
];
break;
case ScreenSizeEnum.SM:
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 5, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/lnd/onchain', icon: this.faChartPie, title: 'Balances', cols: 5, rows: 1 },
{ id: 'fee', goTo: 'Routing', link: '/lnd/routing', icon: this.faBolt, title: 'Routing Fee', cols: 5, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/lnd/connections', icon: this.faNetworkWired, title: 'Channels', cols: 5, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/lnd/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 5, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 5, rows: 1 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 5, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'inactive'], icon: this.faNetworkWired, title: 'Channels', cols: 5, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/lnd/onchain', icon: this.faChartPie, title: 'Balances', cols: 3, rows: 4 },
{ id: 'transactions', goTo: 'Transactions', link: '/lnd/transactions', title: '', cols: 3, rows: 4 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/lnd/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 3, rows: 8 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/lnd/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 3, rows: 8 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 3, rows: 4 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 3, rows: 4 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 3, rows: 8 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 3, rows: 8 }
];
break;
case ScreenSizeEnum.MD:
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 5, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/lnd/onchain', icon: this.faChartPie, title: 'Balances', cols: 5, rows: 1 },
{ id: 'fee', goTo: 'Routing', link: '/lnd/routing', icon: this.faBolt, title: 'Routing Fee', cols: 5, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/lnd/connections', icon: this.faNetworkWired, title: 'Channels', cols: 5, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/lnd/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 5, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 5, rows: 1 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 5, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'inactive'], icon: this.faNetworkWired, title: 'Channels', cols: 5, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 10, rows: 2 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/lnd/onchain', icon: this.faChartPie, title: 'Balances', cols: 3, rows: 4 },
{ id: 'transactions', goTo: 'Transactions', link: '/lnd/transactions', title: '', cols: 3, rows: 4 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/lnd/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 3, rows: 8 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/lnd/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 3, rows: 8 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 3, rows: 4 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 3, rows: 4 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 3, rows: 8 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 3, rows: 8 }
];
break;
default:
this.operatorCards = [
{ id: 'node', icon: this.faServer, title: 'Node Information', cols: 3, rows: 1 },
{ id: 'balance', goTo: 'On-Chain', link: '/lnd/onchain', icon: this.faChartPie, title: 'Balances', cols: 3, rows: 1 },
{ id: 'capacity', goTo: 'Channels', link: '/lnd/connections', icon: this.faNetworkWired, title: 'Channels Capacity', cols: 4, rows: 2 },
{ id: 'fee', goTo: 'Routing', link: '/lnd/routing', icon: this.faBolt, title: 'Routing Fee', cols: 3, rows: 1 },
{ id: 'status', goTo: 'Channels', link: '/lnd/connections', icon: this.faNetworkWired, title: 'Channels', cols: 3, rows: 1 }
{ id: 'node', goToOptions: [], links: [], icon: this.faServer, title: 'Node Information', cols: 3, rows: 1 },
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 3, rows: 1 },
{ id: 'capacity', goToOptions: ['Channels'], links: ['connections'], icon: this.faNetworkWired, title: 'Channels Capacity', cols: 4, rows: 2 },
{ id: 'fee', goToOptions: ['Routing', 'Fees Summary'], links: ['routing', 'reports'], icon: this.faBolt, title: 'Routing Fee', cols: 3, rows: 1 },
{ id: 'status', goToOptions: ['Channels', 'Inactive Channels'], links: ['connections', 'inactive'], icon: this.faNetworkWired, title: 'Channels', cols: 3, rows: 1 }
];
this.merchantCards = [
{ id: 'balance', goTo: 'On-Chain', link: '/lnd/onchain', icon: this.faChartPie, title: 'Balances', cols: 2, rows: 5 },
{ id: 'inboundLiq', goTo: 'Channels', link: '/lnd/connections', icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'outboundLiq', goTo: 'Channels', link: '/lnd/connections', icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'transactions', goTo: 'Transactions', link: '/lnd/transactions', title: '', cols: 2, rows: 5 }
{ id: 'balance', goToOptions: ['On-Chain'], links: ['onchain'], icon: this.faChartPie, title: 'Balances', cols: 2, rows: 5 },
{ id: 'inboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleDown, title: 'In-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'outboundLiq', goToOptions: ['Channels'], links: ['connections'], icon: this.faAngleDoubleUp, title: 'Out-Bound Liquidity', cols: 2, rows: 10 },
{ id: 'transactions', goToOptions: ['Transactions', 'Transactions Summary'], links: ['transactions', 'reports/transactions'], title: '', cols: 2, rows: 5 }
];
break;
}
@ -236,7 +236,11 @@ export class HomeComponent implements OnInit, OnDestroy {
}
onNavigateTo(link: string) {
this.router.navigateByUrl(link);
if (link === 'inactive') {
this.router.navigateByUrl('/lnd/connections', { state: { filter: link } });
} else {
this.router.navigateByUrl('/lnd/' + link);
}
}
onsortChannelsBy() {

@ -28,6 +28,7 @@ import { TransactionsReportComponent } from './reports/transactions/transactions
import { RoutingComponent } from './routing/routing.component';
import { ForwardingHistoryComponent } from './routing/forwarding-history/forwarding-history.component';
import { RoutingPeersComponent } from './routing/routing-peers/routing-peers.component';
import { NonRoutingPeersComponent } from './routing/non-routing-peers/non-routing-peers.component';
import { ChannelLookupComponent } from './graph/lookups/channel-lookup/channel-lookup.component';
import { NodeLookupComponent } from './graph/lookups/node-lookup/node-lookup.component';
import { BackupComponent } from './backup/backup.component';
@ -85,6 +86,7 @@ import { LNDUnlockedGuard } from '../shared/services/auth.guard';
RoutingComponent,
ForwardingHistoryComponent,
RoutingPeersComponent,
NonRoutingPeersComponent,
ChannelLookupComponent,
NodeLookupComponent,
BackupComponent,

@ -36,6 +36,7 @@ import { VerifyComponent } from './sign-verify-message/verify/verify.component';
import { NotFoundComponent } from '../shared/components/not-found/not-found.component';
import { AuthGuard, LNDUnlockedGuard } from '../shared/services/auth.guard';
import { NonRoutingPeersComponent } from './routing/non-routing-peers/non-routing-peers.component';
export const LndRoutes: Routes = [
{
@ -92,7 +93,8 @@ export const LndRoutes: Routes = [
path: 'routing', component: RoutingComponent, canActivate: [LNDUnlockedGuard], children: [
{ path: '', pathMatch: 'full', redirectTo: 'forwardinghistory' },
{ path: 'forwardinghistory', component: ForwardingHistoryComponent, canActivate: [LNDUnlockedGuard] },
{ path: 'peers', component: RoutingPeersComponent, canActivate: [LNDUnlockedGuard] }
{ path: 'peers', component: RoutingPeersComponent, canActivate: [LNDUnlockedGuard] },
{ path: 'nonroutingprs', component: NonRoutingPeersComponent, canActivate: [LNDUnlockedGuard] }
]
},
{

@ -214,7 +214,11 @@ export class ChannelRebalanceComponent implements OnInit, OnDestroy {
sendPayment(payReq: string) {
this.flgInvoiceGenerated = true;
this.paymentRequest = payReq;
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.NO_SPINNER, paymentReq: payReq, outgoingChannel: this.selChannel, feeLimitType: this.feeFormGroup.controls.selFeeLimitType.value.id, feeLimit: this.feeFormGroup.controls.feeLimit.value, allowSelfPayment: true, lastHopPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey, fromDialog: true } }));
if (this.feeFormGroup.controls.selFeeLimitType.value.id === 'percent' && !(+this.feeFormGroup.controls.feeLimit.value % 1 === 0)) {
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.NO_SPINNER, paymentReq: payReq, outgoingChannel: this.selChannel, feeLimitType: 'fixed', feeLimit: Math.ceil((+this.feeFormGroup.controls.feeLimit.value * +this.inputFormGroup.controls.rebalanceAmount.value) / 100), allowSelfPayment: true, lastHopPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey, fromDialog: true } }));
} else {
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.NO_SPINNER, paymentReq: payReq, outgoingChannel: this.selChannel, feeLimitType: this.feeFormGroup.controls.selFeeLimitType.value.id, feeLimit: this.feeFormGroup.controls.feeLimit.value, allowSelfPayment: true, lastHopPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey, fromDialog: true } }));
}
}
filterActiveChannels() {

@ -6,7 +6,7 @@
</mat-form-field>
</div>
<div [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container">
<mat-progress-bar *ngIf="apiCallStatus.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<mat-progress-bar *ngIf="apiCallStatus?.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table mat-table #table fxFlex="100" [dataSource]="channels" matSort [ngClass]="{'overflow-auto error-border': errorMessage !== '','overflow-auto': true}">
<ng-container matColumnDef="remote_alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Peer </th>
@ -75,10 +75,10 @@
</ng-container>
<ng-container matColumnDef="no_peer">
<td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="numPeers<1 && (!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.COMPLETED">No peers connected. Add a peer in order to open a channel.</p>
<p *ngIf="numPeers>0 && (!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.COMPLETED">No channel available.</p>
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.INITIATED">Getting channels...</p>
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.ERROR">{{errorMessage}}</p>
<p *ngIf="numPeers<1 && (!channels?.data || channels?.data?.length<1) && apiCallStatus?.status === apiCallStatusEnum.COMPLETED">No peers connected. Add a peer in order to open a channel.</p>
<p *ngIf="numPeers>0 && (!channels?.data || channels?.data?.length<1) && apiCallStatus?.status === apiCallStatusEnum.COMPLETED">No channel available.</p>
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus?.status === apiCallStatusEnum.INITIATED">Getting channels...</p>
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus?.status === apiCallStatusEnum.ERROR">{{errorMessage}}</p>
</td>
</ng-container>
<tr mat-footer-row *matFooterRowDef="['no_peer']" [ngClass]="{'display-none': numPeers>0 && channels?.data && channels?.data?.length>0}"></tr>

@ -1,4 +1,5 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { StoreModule } from '@ngrx/store';
import { RootReducer } from '../../../../../store/rtl.reducers';
@ -11,11 +12,12 @@ import { LoopService } from '../../../../../shared/services/loop.service';
import { ChannelOpenTableComponent } from './channel-open-table.component';
import { SharedModule } from '../../../../../shared/shared.module';
import { mockDataService, mockLoggerService, mockLNDEffects, mockRTLEffects } from '../../../../../shared/test-helpers/mock-services';
import { mockDataService, mockLoggerService, mockLNDEffects, mockRTLEffects, mockRouter } from '../../../../../shared/test-helpers/mock-services';
import { RTLEffects } from '../../../../../store/rtl.effects';
import { LNDEffects } from '../../../../store/lnd.effects';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DataService } from '../../../../../shared/services/data.service';
import { Router } from '@angular/router';
describe('ChannelOpenTableComponent', () => {
let component: ChannelOpenTableComponent;
@ -27,11 +29,13 @@ describe('ChannelOpenTableComponent', () => {
imports: [
BrowserAnimationsModule,
SharedModule,
RouterTestingModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cl: CLReducer, ecl: ECLReducer })
],
providers: [
CommonService,
{ provide: LoggerService, useClass: mockLoggerService }, LoopService,
CommonService, LoopService,
{ provide: Router, useClass: mockRouter },
{ provide: LoggerService, useClass: mockLoggerService },
{ provide: DataService, useClass: mockDataService },
{ provide: RTLEffects, useClass: mockRTLEffects },
{ provide: LNDEffects, useClass: mockLNDEffects }

@ -1,4 +1,5 @@
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
import { Router } from '@angular/router';
import { DecimalPipe } from '@angular/common';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
@ -64,7 +65,7 @@ export class ChannelOpenTableComponent implements OnInit, AfterViewInit, OnDestr
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<RTLState>, private lndEffects: LNDEffects, private commonService: CommonService, private rtlEffects: RTLEffects, private decimalPipe: DecimalPipe, private loopService: LoopService) {
constructor(private logger: LoggerService, private store: Store<RTLState>, private lndEffects: LNDEffects, private commonService: CommonService, private rtlEffects: RTLEffects, private decimalPipe: DecimalPipe, private loopService: LoopService, private router: Router) {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
@ -79,6 +80,7 @@ export class ChannelOpenTableComponent implements OnInit, AfterViewInit, OnDestr
this.flgSticky = true;
this.displayedColumns = ['remote_alias', 'uptime', 'total_satoshis_sent', 'total_satoshis_received', 'local_balance', 'remote_balance', 'balancedness', 'actions'];
}
this.selFilter = this.router.getCurrentNavigation().extras?.state?.filter ? this.router.getCurrentNavigation().extras?.state?.filter : '';
}
ngOnInit() {
@ -290,6 +292,7 @@ export class ChannelOpenTableComponent implements OnInit, AfterViewInit, OnDestr
this.channels.sort = this.sort;
this.channels.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null);
this.channels.paginator = this.paginator;
this.applyFilter();
this.logger.info(this.channels);
}

@ -0,0 +1,59 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch" class="padding-gap-x-large">
<div class="p-2 error-border my-2" *ngIf="errorMessage !== ''">{{errorMessage}}</div>
<div *ngIf="errorMessage === ''" fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch">
<div *ngIf="errorMessage === ''" fxLayout="column" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70">Non Routing Peers</div>
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter()" [(ngModel)]="filter" placeholder="Filter">
</mat-form-field>
</div>
<div *ngIf="errorMessage === ''" [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container">
<mat-progress-bar *ngIf="apiCallStatus.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="NonRoutingPeers" matSort class="overflow-auto">
<ng-container matColumnDef="chan_id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Channel ID</th>
<td mat-cell *matCellDef="let nonRPeer" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '28rem'}">{{nonRPeer.chan_id}}</td>
</ng-container>
<ng-container matColumnDef="remote_alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Peer Alias</th>
<td mat-cell *matCellDef="let nonRPeer" [ngStyle]="{'max-width': (screenSize === screenSizeEnum.XS) ? '10rem' : '28rem'}">{{nonRPeer.remote_alias}}</td>
</ng-container>
<ng-container matColumnDef="total_satoshis_sent">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Sats Sent</th>
<td mat-cell *matCellDef="let nonRPeer"><span fxLayoutAlign="end center">{{nonRPeer.total_satoshis_sent | number}}</span></td>
</ng-container>
<ng-container matColumnDef="total_satoshis_received">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Sats Received</th>
<td mat-cell *matCellDef="let nonRPeer"><span fxLayoutAlign="end center">{{nonRPeer.total_satoshis_received | number}}</span>
</td>
</ng-container>
<ng-container matColumnDef="local_balance">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Local Balance (Sats)</th>
<td mat-cell *matCellDef="let nonRPeer"><span fxLayoutAlign="end center">{{nonRPeer.local_balance | number}}</span>
</td>
</ng-container>
<ng-container matColumnDef="remote_balance">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Remote Balance (Sats)</th>
<td mat-cell *matCellDef="let nonRPeer"><span fxLayoutAlign="end center">{{nonRPeer.remote_balance | number}}</span></td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="pr-3"><span fxLayoutAlign="end center">Actions</span></th>
<td mat-cell *matCellDef="let nonRPeer" class="pl-2" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4" (click)="onManagePeer(nonRPeer)">Manage</button>
</td>
</ng-container>
<ng-container matColumnDef="no_non_routing_event">
<td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="(!NonRoutingPeers?.data || NonRoutingPeers?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.COMPLETED">All peers are routing.</p>
<p *ngIf="(!NonRoutingPeers?.data || NonRoutingPeers?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.INITIATED">Getting non routing peers...</p>
<p *ngIf="(!NonRoutingPeers?.data || NonRoutingPeers?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.ERROR">{{errorMessage}}</p>
</td>
</ng-container>
<tr mat-footer-row *matFooterRowDef="['no_non_routing_event']" [ngClass]="{'display-none': NonRoutingPeers?.data?.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<mat-paginator #paginator [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" [showFirstLastButtons]="screenSize === screenSizeEnum.XS ? false : true" class="mb-1"></mat-paginator>
</div>
</div>

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

@ -0,0 +1,53 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { StoreModule } from '@ngrx/store';
import { RouterTestingModule } from '@angular/router/testing';
import { RootReducer } from '../../../store/rtl.reducers';
import { LNDReducer } from '../../store/lnd.reducers';
import { CLReducer } from '../../../clightning/store/cl.reducers';
import { ECLReducer } from '../../../eclair/store/ecl.reducers';
import { CommonService } from '../../../shared/services/common.service';
import { LoggerService } from '../../../shared/services/logger.service';
import { NonRoutingPeersComponent } from './non-routing-peers.component';
import { mockDataService, mockLoggerService } from '../../../shared/test-helpers/mock-services';
import { SharedModule } from '../../../shared/shared.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DataService } from '../../../shared/services/data.service';
describe('NonRoutingPeersComponent', () => {
let component: NonRoutingPeersComponent;
let fixture: ComponentFixture<NonRoutingPeersComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [NonRoutingPeersComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
RouterTestingModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cl: CLReducer, ecl: ECLReducer })
],
providers: [
CommonService,
{ provide: LoggerService, useClass: mockLoggerService },
{ provide: DataService, useClass: mockDataService }
]
}).
compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NonRoutingPeersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
afterEach(() => {
TestBed.resetTestingModule();
});
});

@ -0,0 +1,154 @@
import { Component, OnInit, ViewChild, OnDestroy, AfterViewInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { ForwardingEvent, SwitchRes, Channel, ChannelsSummary, LightningBalance } from '../../../shared/models/lndModels';
import { APICallStatusEnum, 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';
import { CommonService } from '../../../shared/services/common.service';
import { RTLState } from '../../../store/rtl.state';
import { channels, forwardingHistory } from '../../store/lnd.selector';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'rtl-non-routing-peers',
templateUrl: './non-routing-peers.component.html',
styleUrls: ['./non-routing-peers.component.scss'],
providers: [
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Non routing peers') }
]
})
export class NonRoutingPeersComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | undefined;
public routingPeersData = [];
public displayedColumns: any[] = [];
public NonRoutingPeers = new MatTableDataSource<any>([]);
public flgSticky = false;
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
public errorMessage = '';
public filter = '';
public activeChannels: Channel[] = [];
public apiCallStatus: ApiCallStatusPayload = null;
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private router: Router, private activatedRoute: ActivatedRoute) {
this.screenSize = this.commonService.getScreenSize();
if (this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
this.displayedColumns = ['remote_alias', 'local_balance', 'remote_balance', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false;
this.displayedColumns = ['remote_alias', 'local_balance', 'remote_balance', 'actions'];
} else if (this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['chan_id', 'remote_alias', 'local_balance', 'remote_balance', 'actions'];
} else {
this.flgSticky = true;
this.displayedColumns = ['chan_id', 'remote_alias', 'total_satoshis_received', 'total_satoshis_sent', 'local_balance', 'remote_balance', 'actions'];
}
}
ngOnInit() {
this.store.select(forwardingHistory).pipe(takeUntil(this.unSubs[0])).
subscribe((fhSelector: { forwardingHistory: SwitchRes, apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = fhSelector.apiCallStatus;
if (fhSelector.apiCallStatus?.status === APICallStatusEnum.ERROR) {
this.errorMessage = (typeof (this.apiCallStatus.message) === 'object') ? JSON.stringify(this.apiCallStatus.message) : this.apiCallStatus.message;
}
if (fhSelector.forwardingHistory.forwarding_events) {
this.routingPeersData = fhSelector.forwardingHistory.forwarding_events;
} else {
this.routingPeersData = [];
}
if (this.routingPeersData.length > 0 && this.sort && this.paginator) {
this.loadNonRoutingPeersTable(this.routingPeersData);
}
this.logger.info(fhSelector.apiCallStatus);
this.logger.info(fhSelector.forwardingHistory);
});
this.store.select(channels).pipe(takeUntil(this.unSubs[1])).
subscribe((channelsSelector: { channels: Channel[], channelsSummary: ChannelsSummary, lightningBalance: LightningBalance, apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = channelsSelector.apiCallStatus;
if (this.apiCallStatus.status === APICallStatusEnum.ERROR) {
this.errorMessage = (typeof (this.apiCallStatus.message) === 'object') ? JSON.stringify(this.apiCallStatus.message) : this.apiCallStatus.message;
}
this.activeChannels = channelsSelector.channels;
this.logger.info(channelsSelector);
});
}
ngAfterViewInit() {
if (this.routingPeersData.length > 0) {
this.loadNonRoutingPeersTable(this.routingPeersData);
}
}
onManagePeer(selNonRoutingChannel: Channel) {
this.router.navigate(['../../', 'connections', 'channels', 'open'], { relativeTo: this.activatedRoute, state: { filter: selNonRoutingChannel.chan_id } });
}
// groupRoutingPeers(forwardingEvents: ForwardingEvent[]) {
// const results: any[] = [];
// forwardingEvents.forEach((event: ForwardingEvent) => {
// const foundEntryInIdx = results.findIndex((result) => result.chan_id === event.chan_id_in);
// const foundEntryOutIdx = results.findIndex((result) => result.chan_id === event.chan_id_out);
// if (foundEntryInIdx < 0 && foundEntryOutIdx < 0) {
// results.push({ chan_id: event.chan_id_in, alias: event.alias_in, amt_in_msat: +event.amt_in_msat, amt_out_msat: 0 });
// results.push({ chan_id: event.chan_id_out, alias: event.alias_out, amt_out_msat: +event.amt_out_msat, amt_in_msat: 0 });
// }
// if (foundEntryInIdx < 0 && foundEntryOutIdx > -1) {
// results.push({ chan_id: event.chan_id_in, alias: event.alias_in, amt_in_msat: +event.amt_in_msat, amt_out_msat: 0 });
// results[foundEntryOutIdx].amt_out_msat = results[foundEntryOutIdx].amt_out_msat + +event.amt_out_msat;
// }
// if (foundEntryInIdx > -1 && foundEntryOutIdx < 0) {
// results.push({ chan_id: event.chan_id_out, alias: event.alias_out, amt_out_msat: +event.amt_out_msat, amt_in_msat: 0 });
// results[foundEntryInIdx].amt_in_msat = results[foundEntryInIdx].amt_in_msat + +event.amt_in_msat;
// }
// if (foundEntryInIdx > -1 && foundEntryOutIdx > -1) {
// results[foundEntryInIdx].amt_in_msat = results[foundEntryInIdx].amt_in_msat + +event.amt_in_msat;
// results[foundEntryOutIdx].amt_out_msat = results[foundEntryOutIdx].amt_out_msat + +event.amt_out_msat;
// }
// });
// return this.commonService.sortDescByKey(results, 'alias');
// }
loadNonRoutingPeersTable(forwardingEvents: ForwardingEvent[]) {
if (forwardingEvents.length > 0) {
// const grpdRoutingPeers = this.groupRoutingPeers(forwardingEvents);
const filteredNonRoutingChannels = this.activeChannels.filter((actvChnl) => forwardingEvents.findIndex((evnt) => (evnt.chan_id_in === actvChnl.chan_id || evnt.chan_id_out === actvChnl.chan_id)) < 0);
this.NonRoutingPeers = new MatTableDataSource<Channel>(filteredNonRoutingChannels);
this.NonRoutingPeers.sort = this.sort;
this.NonRoutingPeers.filterPredicate = (nrchnl: Channel, fltr: string) => JSON.stringify(nrchnl).toLowerCase().includes(fltr);
this.NonRoutingPeers.paginator = this.paginator;
this.logger.info(this.NonRoutingPeers);
} else {
this.NonRoutingPeers = new MatTableDataSource<Channel>([]);
}
this.applyFilter();
}
applyFilter() {
this.NonRoutingPeers.filter = this.filter.toLowerCase();
}
ngOnDestroy() {
this.unSubs.forEach((completeSub) => {
completeSub.next(null);
completeSub.complete();
});
}
}

@ -20,7 +20,7 @@ import { forwardingHistory } from '../../store/lnd.selector';
templateUrl: './routing-peers.component.html',
styleUrls: ['./routing-peers.component.scss'],
providers: [
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Peers') }
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Routing peers') }
]
})
export class RoutingPeersComponent implements OnInit, AfterViewInit, OnDestroy {

@ -7,6 +7,10 @@ import { faMapSigns } from '@fortawesome/free-solid-svg-icons';
import { RTLState } from '../../store/rtl.state';
import { getForwardingHistory, setForwardingHistory } from '../store/lnd.actions';
import { channels } from '../store/lnd.selector';
import { Channel, ChannelsSummary, LightningBalance } from '../../shared/models/lndModels';
import { ApiCallStatusPayload } from '../../shared/models/apiCallsPayload';
import { LoggerService } from '../../shared/services/logger.service';
@Component({
selector: 'rtl-routing',
@ -21,11 +25,11 @@ export class RoutingComponent implements OnInit, OnDestroy {
public yesterday = new Date(this.today.getFullYear(), this.today.getMonth(), this.today.getDate() - 1, 0, 0, 0);
public endDate = this.today;
public startDate = this.lastMonthDay;
public links = [{ link: 'forwardinghistory', name: 'Forwarding History' }, { link: 'peers', name: 'Routing Peers' }];
public links = [{ link: 'forwardinghistory', name: 'Forwarding History' }, { link: 'peers', name: 'Routing Peers' }, { link: 'nonroutingprs', name: 'Non Routing Peers' }];
public activeLink = this.links[0].link;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private store: Store<RTLState>, private router: Router) { }
constructor(private logger: LoggerService, private store: Store<RTLState>, private router: Router) { }
ngOnInit() {
this.onEventsFetch();

@ -1166,16 +1166,16 @@ export class LNDEffects implements OnDestroy {
newRoute = '/lnd/home';
}
this.router.navigate([newRoute]);
this.store.dispatch(fetchFees()); // Fetches monthly forwarding history as well, to count total number of events
this.store.dispatch(fetchBalanceBlockchain());
this.store.dispatch(fetchChannels());
this.store.dispatch(fetchPendingChannels());
this.store.dispatch(fetchClosedChannels());
this.store.dispatch(fetchPeers());
this.store.dispatch(fetchNetwork());
this.store.dispatch(getAllLightningTransactions());
this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: 10, reversed: true } }));
this.store.dispatch(fetchPayments({ payload: { max_payments: 10, reversed: true } }));
this.store.dispatch(fetchFees()); // Fetches monthly forwarding history as well, to count total number of events
this.store.dispatch(getAllLightningTransactions());
}
handleErrorWithoutAlert(actionName: string, uiMessage: string, genericErrorMessage: string, err: { status: number, error: any }) {

@ -2,6 +2,7 @@ import { createReducer, on } from '@ngrx/store';
import { initLNDState } from './lnd.state';
import { addInvoice, removeChannel, removePeer, resetLNDStore, setChannels, setAllLightningTransactions, setBalanceBlockchain, setChildNodeSettingsLND, setClosedChannels, setFees, setForwardingHistory, setInfo, setInvoices, setNetwork, setPayments, setPeers, setPendingChannels, setTransactions, setUTXOs, updateLNDAPICallStatus, updateInvoice } from './lnd.actions';
import { Channel, ClosedChannel } from '../../shared/models/lndModels';
let flgTransactionsSet = false;
let flgUTXOsSet = false;
@ -185,38 +186,10 @@ export const LNDReducer = createReducer(initLNDState,
allLightningTransactions: payload
})),
on(setForwardingHistory, (state, { payload }) => {
const updatedPayload = !payload.forwarding_events ? {} : JSON.parse(JSON.stringify(payload));
const storedChannels = [...state.channels, ...state.closedChannels];
let updatedPayload = !payload.forwarding_events ? {} : JSON.parse(JSON.stringify(payload));
if (updatedPayload.forwarding_events) {
const storedChannels = [...state.channels, ...state.closedChannels];
updatedPayload.forwarding_events.forEach((fhEvent) => {
if (storedChannels && storedChannels.length > 0) {
for (let idx = 0; idx < storedChannels.length; idx++) {
if (storedChannels[idx].chan_id.toString() === fhEvent.chan_id_in) {
fhEvent.alias_in = storedChannels[idx].remote_alias ? storedChannels[idx].remote_alias : fhEvent.chan_id_in;
if (fhEvent.alias_out) {
return;
}
}
if (storedChannels[idx].chan_id.toString() === fhEvent.chan_id_out) {
fhEvent.alias_out = storedChannels[idx].remote_alias ? storedChannels[idx].remote_alias : fhEvent.chan_id_out;
if (fhEvent.alias_in) {
return;
}
}
if (idx === storedChannels.length - 1) {
if (!fhEvent.alias_in) {
fhEvent.alias_in = fhEvent.chan_id_in;
}
if (!fhEvent.alias_out) {
fhEvent.alias_out = fhEvent.chan_id_out;
}
}
}
} else {
fhEvent.alias_in = fhEvent.chan_id_in;
fhEvent.alias_out = fhEvent.chan_id_out;
}
});
updatedPayload = mapAliases(updatedPayload, storedChannels);
}
return {
...state,
@ -224,3 +197,36 @@ export const LNDReducer = createReducer(initLNDState,
};
})
);
const mapAliases = (payload: any, storedChannels: (Channel | ClosedChannel)[]) => {
payload.forwarding_events.forEach((fhEvent) => {
if (storedChannels && storedChannels.length > 0) {
for (let idx = 0; idx < storedChannels.length; idx++) {
if (storedChannels[idx].chan_id.toString() === fhEvent.chan_id_in) {
fhEvent.alias_in = storedChannels[idx].remote_alias ? storedChannels[idx].remote_alias : fhEvent.chan_id_in;
if (fhEvent.alias_out) {
return;
}
}
if (storedChannels[idx].chan_id.toString() === fhEvent.chan_id_out) {
fhEvent.alias_out = storedChannels[idx].remote_alias ? storedChannels[idx].remote_alias : fhEvent.chan_id_out;
if (fhEvent.alias_in) {
return;
}
}
if (idx === storedChannels.length - 1) {
if (!fhEvent.alias_in) {
fhEvent.alias_in = fhEvent.chan_id_in;
}
if (!fhEvent.alias_out) {
fhEvent.alias_out = fhEvent.chan_id_out;
}
}
}
} else {
fhEvent.alias_in = fhEvent.chan_id_in;
fhEvent.alias_out = fhEvent.chan_id_out;
}
});
return payload;
};

@ -2,6 +2,7 @@
.login-container {
height: 90vh;
padding-right: 11rem;
& .mat-card {
height: 30rem;

@ -5,20 +5,20 @@
<span class="page-title">Password</span>
</div>
<mat-form-field>
<input matInput placeholder="Current Password" type="password" id="currpassword" name="currpassword" [(ngModel)]="currPassword" tabindex="6" required>
<input autoFocus matInput placeholder="Current Password" type="password" id="currpassword" name="currpassword" [(ngModel)]="currPassword" tabindex="1" required>
<mat-error *ngIf="!currPassword">Current password is required.</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="New Password" type="password" id="newpassword" name="newpassword" [(ngModel)]="newPassword" tabindex="7" required>
<input matInput placeholder="New Password" type="password" id="newpassword" name="newpassword" [(ngModel)]="newPassword" tabindex="2" required>
<mat-error *ngIf="matchOldAndNewPasswords()">{{errorMsg}}</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Confirm New Password" type="password" id="confirmpassword" name="confirmpassword" [(ngModel)]="confirmPassword" tabindex="8" required>
<input matInput placeholder="Confirm New Password" type="password" id="confirmpassword" name="confirmpassword" [(ngModel)]="confirmPassword" tabindex="3" required>
<mat-error *ngIf="matchNewPasswords()">{{errorConfirmMsg}}</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="start start" class="mt-1">
<button class="mr-1" mat-stroked-button color="primary" type="reset" (click)="onResetPassword()" tabindex="9">Reset</button>
<button mat-flat-button color="primary" tabindex="10" type="button" (click)="onChangePassword()">Change Password</button>
<button class="mr-1" mat-stroked-button color="primary" type="reset" (click)="onResetPassword()" tabindex="4">Reset</button>
<button mat-flat-button color="primary" tabindex="5" type="submit" (click)="onChangePassword()">Change Password</button>
</div>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="end stretch" class="my-2">
<mat-divider [inset]="true"></mat-divider>
@ -34,7 +34,7 @@
<span>Protect your account from unauthorized access by requiring a second authentication method in addition to your password.</span>
</div>
<div class="mt-1">
<button mat-flat-button color="primary" tabindex="11" (click)="on2FAuth()" class="mb-2">{{appConfig.enable2FA ? 'Disable 2FA' : 'Enable 2FA'}}</button>
<button mat-flat-button color="primary" tabindex="6" (click)="on2FAuth()" class="mb-2">{{appConfig.enable2FA ? 'Disable 2FA' : 'Enable 2FA'}}</button>
</div>
</div>
</div>

@ -56,6 +56,7 @@ export interface ApiCallsListCL {
FetchPayments: ApiCallStatusPayload;
FetchForwardingHistory: ApiCallStatusPayload;
FetchFailedForwardingHistory: ApiCallStatusPayload;
FetchLocalFailedForwardingHistory: ApiCallStatusPayload;
FetchOffers: ApiCallStatusPayload;
FetchOfferBookmarks: ApiCallStatusPayload;
}

@ -272,6 +272,17 @@ export interface ForwardingEvent {
resolved_time?: number;
}
export interface LocalFailedEvent {
in_channel?: string;
in_channel_alias?: string;
in_msatoshi?: number;
in_msat?: string;
status?: string;
received_time?: number;
failcode?: number;
failreason?: string;
}
export interface Routes {
id?: string;
channel?: string;

@ -72,11 +72,18 @@ export class CommonService implements OnDestroy {
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (word.toUpperCase())).replace(/\s+/g, '').replace(/-/g, ' ');
}
titleCase(str) {
titleCase(str: string, searchValue?: string, replaceValue?: string) {
if (searchValue && replaceValue && searchValue !== '' && replaceValue !== '') {
str = str.replace(new RegExp(searchValue, 'g'), replaceValue);
}
if (str.indexOf('!\n') > 0 || str.indexOf('.\n') > 0) {
return str.split('\n').reduce((accumulator, currentStr) => accumulator + currentStr.charAt(0).toUpperCase() + currentStr.substring(1).toLowerCase() + '\n', '');
} else {
return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase();
if (str.indexOf(' ') > 0) {
return str.split(' ').reduce((accumulator, currentStr) => accumulator + currentStr.charAt(0).toUpperCase() + currentStr.substring(1).toLowerCase() + ' ', '');
} else {
return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase();
}
}
}

@ -146,6 +146,32 @@ export const WALLET_ADDRESS_TYPE = {
UNUSED_NESTED_PUBKEY_HASH: { name: 'Unused Nested Pubkey Hash', tooltip: '' }
};
export enum CLFailReason {
WIRE_INVALID_ONION_VERSION = 'Invalid Onion Version',
WIRE_INVALID_ONION_HMAC = 'Invalid Onion HMAC',
WIRE_INVALID_ONION_KEY = 'Invalid Onion Key',
WIRE_TEMPORARY_CHANNEL_FAILURE = 'Temporary Channel Failure',
WIRE_PERMANENT_CHANNEL_FAILURE = 'Permanent Channel Failure',
WIRE_REQUIRED_CHANNEL_FEATURE_MISSING = 'Missing Required Channel Feature',
WIRE_UNKNOWN_NEXT_PEER = 'Unknown Next Peer',
WIRE_AMOUNT_BELOW_MINIMUM = 'Amount Below Minimum',
WIRE_FEE_INSUFFICIENT = 'Insufficient Fee',
WIRE_INCORRECT_CLTV_EXPIRY = 'Incorrect CLTV Expiry',
WIRE_EXPIRY_TOO_FAR = 'Expiry Too Far',
WIRE_EXPIRY_TOO_SOON = 'Expiry Too Soon',
WIRE_CHANNEL_DISABLED = 'Channel Disabled',
WIRE_INVALID_ONION_PAYLOAD = 'Invalid Onion Payload',
WIRE_INVALID_REALM = 'Invalid Realm',
WIRE_PERMANENT_NODE_FAILURE = 'Permanent Node Failure',
WIRE_TEMPORARY_NODE_FAILURE = 'Temporary Node Failure',
WIRE_REQUIRED_NODE_FEATURE_MISSING = 'Missing Required Node Feature',
WIRE_INVALID_ONION_BLINDING = 'Invalid Onion Binding',
WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS = 'Incorrect or Unknow Payment Details',
WIRE_MPP_TIMEOUT = 'MPP Timeout',
WIRE_FINAL_INCORRECT_CLTV_EXPIRY = 'Incorrect CLTV Expiry',
WIRE_FINAL_INCORRECT_HTLC_AMOUNT = 'Incorrect HTLC Amount'
}
export enum CLChannelPendingState {
CHANNELD_NORMAL = 'Active',
OPENINGD = 'Opening',
@ -457,6 +483,8 @@ export enum CLActions {
SET_FORWARDING_HISTORY_CL = 'SET_FORWARDING_HISTORY_CL',
GET_FAILED_FORWARDING_HISTORY_CL = 'GET_FAILED_FORWARDING_HISTORY_CL',
SET_FAILED_FORWARDING_HISTORY_CL = 'SET_FAILED_FORWARDING_HISTORY_CL',
GET_LOCAL_FAILED_FORWARDING_HISTORY_CL = 'GET_LOCAL_FAILED_FORWARDING_HISTORY_CL',
SET_LOCAL_FAILED_FORWARDING_HISTORY_CL = 'SET_LOCAL_FAILED_FORWARDING_HISTORY_CL',
FETCH_INVOICES_CL = 'FETCH_INVOICES_CL',
SET_INVOICES_CL = 'SET_INVOICES_CL',
SAVE_NEW_INVOICE_CL = 'SAVE_NEW_INVOICE_CL',

@ -28,6 +28,20 @@ export class mockHttpClient {
}
export class mockRouter {
getCurrentNavigation() {
return {
extras: {
state: {
filter: 'DummyChannelID4325565432212367867'
}
}
};
};
}
export class mockDataService {
private lnImplementation = 'LND';

@ -119,8 +119,8 @@ describe('RTL Root Effects', () => {
expect(storeDispatchSpy.calls.all()[0].args[0]).toEqual(openSpinner({ payload: UI_MESSAGES.UPDATE_SELECTED_NODE }));
expect(storeDispatchSpy.calls.all()[1].args[0]).toEqual(updateRootAPICallStatus({ payload: { action: 'UpdateSelNode', status: APICallStatusEnum.INITIATED } }));
expect(storeDispatchSpy.calls.all()[2].args[0]).toEqual(closeSpinner({ payload: UI_MESSAGES.UPDATE_SELECTED_NODE }));
expect(storeDispatchSpy.calls.all()[3].args[0]).toEqual(openAlert({ payload: { data: { type: 'ERROR', alertTitle: 'Update Selected Node Failed!', message: { code: '500', message: 'Request failed.', URL: environment.CONF_API + '/updateSelNode' }, component: ErrorMessageComponent } } }));
expect(storeDispatchSpy.calls.all()[4].args[0]).toEqual(updateRootAPICallStatus({ payload: { action: 'UpdateSelNode', status: APICallStatusEnum.ERROR, statusCode: '500', message: 'Request failed.', URL: environment.CONF_API + '/updateSelNode' } }));
expect(storeDispatchSpy.calls.all()[3].args[0]).toEqual(openAlert({ payload: { data: { type: 'ERROR', alertTitle: 'Update Selected Node Failed!', message: { code: '500', message: 'Request Failed. ', URL: environment.CONF_API + '/updateSelNode' }, component: ErrorMessageComponent } } }));
expect(storeDispatchSpy.calls.all()[4].args[0]).toEqual(updateRootAPICallStatus({ payload: { action: 'UpdateSelNode', status: APICallStatusEnum.ERROR, statusCode: '500', message: 'Request Failed. ', URL: environment.CONF_API + '/updateSelNode' } }));
expect(storeDispatchSpy).toHaveBeenCalledTimes(5);
done();
setTimeout(() => sub.unsubscribe());

@ -596,12 +596,12 @@ export class RTLEffects implements OnDestroy {
setLoggedInDetails(defaultPassword: boolean, postRes: any) {
this.logger.info('Successfully Authorized!');
this.SetToken(postRes.token);
this.store.dispatch(fetchRTLConfig());
this.sessionService.setItem('defaultPassword', defaultPassword);
if (defaultPassword) {
this.sessionService.setItem('defaultPassword', 'true');
this.store.dispatch(openSnackBar({ payload: 'Reset your password.' }));
this.router.navigate(['/settings/auth']);
} else {
this.store.dispatch(fetchRTLConfig());
}
}

@ -26,4 +26,4 @@ export const environment = {
Web_SOCKET_API: '/ws'
};
export const VERSION = '0.12.1-beta';
export const VERSION = '0.12.2-beta';

@ -26,4 +26,4 @@ export const environment = {
Web_SOCKET_API: '/ws'
};
export const VERSION = '0.12.1-beta';
export const VERSION = '0.12.2-beta';

Loading…
Cancel
Save