Add pending htlcs to all channels tabs #1086

Add pending htlcs to all channels tabs #1086
pull/1235/head
Shahana Farooqui 1 year ago
parent f45bbc4ad2
commit 804ba91d7b

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>html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:100%}@media only screen and (max-width: 56.25em){html{font-size:90%}}@media only screen and (max-width: 37.5em){html{font-size:80%}}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}@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}</style><link rel="stylesheet" href="styles.d31e61a01689a167.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.d31e61a01689a167.css"></noscript></head>
<body>
<rtl-app></rtl-app>
<script src="runtime.513b6ad72ee8ead8.js" type="module"></script><script src="polyfills.9720483e1820202a.js" type="module"></script><script src="main.8519111f4b579c59.js" type="module"></script>
<script src="runtime.7f33f29c10c7c45e.js" type="module"></script><script src="polyfills.9720483e1820202a.js" type="module"></script><script src="main.583912e62d2e15ec.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={},m={};function r(e){var o=m[e];if(void 0!==o)return o.exports;var t=m[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=(o,t,i,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,f]=e[n],c=!0,d=0;d<t.length;d++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[d]))?t.splice(d--,1):(c=!1,f<a&&(a=f));if(c){e.splice(n--,1);var u=i();void 0!==u&&(o=u)}}return o}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,i,f]},r.d=(e,o)=>{for(var t in o)r.o(o,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((o,t)=>(r.f[t](e,o),o),[])),r.u=e=>e+"."+{167:"836d81485f16d9bc",267:"8f996ec2b4b156e0",564:"5cacf70cdd7a222e",636:"c6beed2b2207416a"}[e]+".js",r.miniCssF=e=>{},r.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),(()=>{var e={},o="RTLApp:";r.l=(t,i,f,n)=>{if(e[t])e[t].push(i);else{var a,c;if(void 0!==f)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var l=d[u];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==o+f){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",o+f),a.src=r.tu(t)),e[t]=[i];var s=(g,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(y=>y(b)),g)return g(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=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:o=>o},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(i,f)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=i){var a=new Promise((l,s)=>n=e[i]=[l,s]);f.push(n[2]=a);var c=r.p+r.u(i),d=new Error;r.l(c,l=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var s=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;d.message="Loading chunk "+i+" failed.\n("+s+": "+p+")",d.name="ChunkLoadError",d.type=s,d.request=p,n[1](d)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var o=(i,f)=>{var d,u,[n,a,c]=f,l=0;if(n.some(p=>0!==e[p])){for(d in a)r.o(a,d)&&(r.m[d]=a[d]);if(c)var s=c(r)}for(i&&i(f);l<n.length;l++)r.o(e,u=n[l])&&e[u]&&e[u][0](),e[u]=0;return r.O(s)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))})()})();

@ -0,0 +1 @@
(()=>{"use strict";var e,v={},m={};function r(e){var f=m[e];if(void 0!==f)return f.exports;var t=m[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=(f,t,i,o)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,o]=e[n],c=!0,d=0;d<t.length;d++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[d]))?t.splice(d--,1):(c=!1,o<a&&(a=o));if(c){e.splice(n--,1);var u=i();void 0!==u&&(f=u)}}return f}o=o||0;for(var n=e.length;n>0&&e[n-1][2]>o;n--)e[n]=e[n-1];e[n]=[t,i,o]},r.d=(e,f)=>{for(var t in f)r.o(f,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:f[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((f,t)=>(r.f[t](e,f),f),[])),r.u=e=>e+"."+{167:"836d81485f16d9bc",267:"8f996ec2b4b156e0",315:"0621d0b32a4a191d",636:"c6beed2b2207416a"}[e]+".js",r.miniCssF=e=>{},r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),(()=>{var e={},f="RTLApp:";r.l=(t,i,o,n)=>{if(e[t])e[t].push(i);else{var a,c;if(void 0!==o)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var l=d[u];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==f+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",f+o),a.src=r.tu(t)),e[t]=[i];var s=(g,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(y=>y(b)),g)return g(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=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:f=>f},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(i,o)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=i){var a=new Promise((l,s)=>n=e[i]=[l,s]);o.push(n[2]=a);var c=r.p+r.u(i),d=new Error;r.l(c,l=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var s=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;d.message="Loading chunk "+i+" failed.\n("+s+": "+p+")",d.name="ChunkLoadError",d.type=s,d.request=p,n[1](d)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var f=(i,o)=>{var d,u,[n,a,c]=o,l=0;if(n.some(p=>0!==e[p])){for(d in a)r.o(a,d)&&(r.m[d]=a[d]);if(c)var s=c(r)}for(i&&i(o);l<n.length;l++)r.o(e,u=n[l])&&e[u]&&e[u][0](),e[u]=0;return r.O(s)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(f.bind(null,0)),t.push=f.bind(null,t.push.bind(t))})()})();

@ -58,6 +58,7 @@ import { CLNOffersTableComponent } from './transactions/offers/offers-table/offe
import { CLNOfferBookmarksTableComponent } from './transactions/offers/offer-bookmarks-table/offer-bookmarks-table.component';
import { CLNLiquidityAdsListComponent } from './liquidity-ads/liquidity-ads-list/liquidity-ads-list.component';
import { CLNOpenLiquidityChannelComponent } from './liquidity-ads/open-liquidity-channel-modal/open-liquidity-channel-modal.component';
import { CLNChannelActiveHTLCsTableComponent } from './peers-channels/channels/channels-tables/channel-active-htlcs-table/channel-active-htlcs-table.component';
import { CLNUnlockedGuard } from '../shared/services/auth.guard';
@ -121,7 +122,8 @@ import { CLNUnlockedGuard } from '../shared/services/auth.guard';
CLNOffersTableComponent,
CLNOfferBookmarksTableComponent,
CLNLiquidityAdsListComponent,
CLNOpenLiquidityChannelComponent
CLNOpenLiquidityChannelComponent,
CLNChannelActiveHTLCsTableComponent
],
providers: [
CLNUnlockedGuard

@ -24,6 +24,7 @@ import { CLNVerifyComponent } from './sign-verify-message/verify/verify.componen
import { CLNForwardingHistoryComponent } from './routing/forwarding-history/forwarding-history.component';
import { CLNFailedTransactionsComponent } from './routing/failed-transactions/failed-transactions.component';
import { CLNRoutingPeersComponent } from './routing/routing-peers/routing-peers.component';
import { CLNChannelActiveHTLCsTableComponent } from './peers-channels/channels/channels-tables/channel-active-htlcs-table/channel-active-htlcs-table.component';
import { CLNReportsComponent } from './reports/reports.component';
import { CLNRoutingReportComponent } from './reports/routing/routing-report.component';
@ -59,7 +60,8 @@ export const ClnRoutes: Routes = [
path: 'channels', component: CLNChannelsTablesComponent, canActivate: [CLNUnlockedGuard], children: [
{ path: '', pathMatch: <PathMatch>'full', redirectTo: 'open' },
{ path: 'open', component: CLNChannelOpenTableComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'pending', component: CLNChannelPendingTableComponent, canActivate: [CLNUnlockedGuard] }
{ path: 'pending', component: CLNChannelPendingTableComponent, canActivate: [CLNUnlockedGuard] },
{ path: 'activehtlcs', component: CLNChannelActiveHTLCsTableComponent, canActivate: [CLNUnlockedGuard] }
]
},
{ path: 'peers', component: CLNPeersComponent, data: { sweepAll: false }, canActivate: [CLNUnlockedGuard] }

@ -0,0 +1,146 @@
<div fxLayout="column" class="padding-gap">
<div fxLayout="column" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70"></div>
<div fxFlex.gt-xs="30" fxLayoutAlign.gt-xs="space-between center" fxLayout="row" fxLayoutAlign="space-between stretch">
<mat-form-field fxLayout="column" fxFlex="49">
<mat-label>Filter By</mat-label>
<mat-select tabindex="1" name="filterBy" [(ngModel)]="selFilterBy" (selectionChange)="selFilter=''; applyFilter()">
<perfect-scrollbar><mat-option *ngFor="let column of ['all'].concat(displayedColumns.slice(0, -1))" [value]="column">{{getLabel(column)}}</mat-option></perfect-scrollbar>
</mat-select>
</mat-form-field>
<mat-form-field fxLayout="column" fxFlex="49">
<mat-label>Filter</mat-label>
<input matInput name="filter" [(ngModel)]="selFilter" (input)="applyFilter()" (keyup)="applyFilter()">
</mat-form-field>
</div>
</div>
<div fxLayout="column" fxFlex="100" class="table-container" [perfectScrollbar]>
<mat-progress-bar *ngIf="apiCallStatus.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table #table mat-table fxFlex="100" matSort [matSortActive]="tableSetting.sortBy" [matSortDirection]="tableSetting.sortOrder" [dataSource]="channels" [ngClass]="{'error-border': errorMessage !== ''}">
<!-- Channel Group Row Start -->
<ng-container matColumnDef="amount_msat">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Amount (Sats)</th>
<td *matCellDef="let channel" mat-cell>
<span fxLayoutAlign="start center" class="htlc-row-span">
Active HTLCs: {{channel?.htlcs?.length}}
</span>
<ng-container *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs; index as i" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.amount_msat / 1000 | number:'1.0-2'}}
</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="direction">
<th *matHeaderCellDef mat-header-cell mat-sort-header>Alias/Direction</th>
<td *matCellDef="let channel" mat-cell>
<span fxLayoutAlign="start center" class="htlc-row-span">{{channel?.alias}}</span>
<ng-container *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="start center" class="htlc-row-span">
{{htlc?.direction | titlecase}}
</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="id">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">
<span fxLayoutAlign="end center" class="htlc-row-span">HTLC ID</span>
</th>
<td *matCellDef="let channel" mat-cell>
<span fxLayoutAlign="end center" class="htlc-row-span">{{channel?.id}}</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.id | number}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="expiry">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before">
<span fxLayoutAlign="end center" class="htlc-row-span">Expiry</span>
</th>
<td *matCellDef="let channel" mat-cell>
<span fxLayoutAlign="end center" class="htlc-row-span">{{' '}}</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.expiry | number:'1.0-0'}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before" class="pl-3 htlc-row-span">
<span fxLayoutAlign="end center" class="htlc-row-span">State</span>
</th>
<td *matCellDef="let channel" mat-cell class="pl-3">
<span fxLayoutAlign="end center" class="htlc-row-span">{{' '}}</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.state | camelcaseWithReplace:'_'}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="local_trimmed">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before" class="pl-3 htlc-row-span">
<span fxLayoutAlign="end center" class="htlc-row-span">Local Trimmed</span>
</th>
<td *matCellDef="let channel" mat-cell class="pl-3">
<span fxLayoutAlign="end center" class="htlc-row-span">{{' '}}</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="end center" class="htlc-row-span">
{{htlc?.local_trimmed ? 'Yes' : 'No'}}
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="payment_hash">
<th *matHeaderCellDef mat-header-cell mat-sort-header arrowPosition="before" class="pl-3 htlc-row-span">
<span fxLayoutAlign="end center" class="htlc-row-span">Payment Hash</span>
</th>
<td *matCellDef="let channel" mat-cell class="pl-3">
<span fxLayout="row" class="ellipsis-parent htlc-row-span" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '6rem' : colWidth}">
<span fxLayoutAlign="end center" class="ellipsis-child">{{' '}}</span>
</span>
<span *ngIf="channel.is_expanded">
<span *ngFor="let htlc of channel?.htlcs" fxLayoutAlign="start center" class="ellipsis-parent htlc-row-span" [ngStyle]="{'width': (screenSize === screenSizeEnum.XS) ? '6rem' : colWidth}">
<span class="ellipsis-child">{{htlc?.payment_hash}}</span>
</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef mat-header-cell class="px-2">
<div class="bordered-box table-actions-select" fxLayoutAlign="end center">
<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 *matCellDef="let channel" mat-cell class="px-2" fxLayout="column" fxLayoutAlign="center end">
<span fxLayoutAlign="end center" class="htlc-group-head">
<button mat-flat-button class="btn-htlc-expand" color="primary" type="button" tabindex="5" (click)="channel.is_expanded = !channel.is_expanded">{{channel.is_expanded ? 'Hide' : 'Show'}}</button>
</span>
<div *ngIf="channel.is_expanded">
<div *ngFor="let htlc of channel?.htlcs; index as i" class="htlc-group-details" fxLayoutAlign="end center">
<button mat-stroked-button class="btn-htlc-info" color="primary" type="button" tabindex="6" (click)="onHTLCClick(htlc, channel)">View {{i + 1}}</button>
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="no_channel">
<td *matFooterCellDef mat-footer-cell colspan="4">
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.COMPLETED">No active htlc available.</p>
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.INITIATED">Getting active htlcs...</p>
<p *ngIf="(!channels?.data || channels?.data?.length<1) && apiCallStatus.status === apiCallStatusEnum.ERROR">{{errorMessage}}</p>
</td>
</ng-container>
<!-- channel Group Row End -->
<tr *matFooterRowDef="['no_channel']" mat-footer-row [ngClass]="{'display-none': channels?.data && channels?.data?.length>0}"></tr>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns;" mat-row></tr>
</table>
</div>
<mat-paginator class="mb-1" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" [showFirstLastButtons]="screenSize === screenSizeEnum.XS ? false : true"></mat-paginator>
</div>

@ -0,0 +1,34 @@
@import "../../../../../shared/theme/styles/constants.scss";
.mat-column-amount_msat {
.htlc-row-span:not(:first-of-type) {
padding-left: 2rem;
padding-right: 2rem;
}
}
.htlc-row-span {
min-height: 3rem;
&.ellipsis-parent {
display: flex;
align-items: center;
}
}
.mat-column-actions {
& .htlc-group-head, & .htlc-group-details {
min-height: 3rem;
}
& .btn-htlc-expand {
min-width: $table-actions-min-width;
width: $table-actions-min-width;
margin: 0;
}
& .btn-htlc-info {
min-width: $table-actions-min-width - 1rem;
min-width: $table-actions-min-width - 1rem;
margin: 0;
}
}

@ -0,0 +1,51 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { StoreModule } from '@ngrx/store';
import { RootReducer } from '../../../../../store/rtl.reducers';
import { LNDReducer } from '../../../../../lnd/store/lnd.reducers';
import { CLNReducer } from '../../../../../cln/store/cln.reducers';
import { ECLReducer } from '../../../../../eclair/store/ecl.reducers';
import { CommonService } from '../../../../../shared/services/common.service';
import { LoggerService } from '../../../../../shared/services/logger.service';
import { CLNChannelActiveHTLCsTableComponent } from './channel-active-htlcs-table.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('CLNChannelActiveHTLCsTableComponent', () => {
let component: CLNChannelActiveHTLCsTableComponent;
let fixture: ComponentFixture<CLNChannelActiveHTLCsTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [CLNChannelActiveHTLCsTableComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer })
],
providers: [
CommonService,
{ provide: LoggerService, useClass: mockLoggerService },
{ provide: DataService, useClass: mockDataService }
]
}).
compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLNChannelActiveHTLCsTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
afterEach(() => {
TestBed.resetTestingModule();
});
});

@ -0,0 +1,243 @@
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { CLNChannelInformationComponent } from '../../channel-information-modal/channel-information.component';
import { Channel, ChannelHTLC } from '../../../../../shared/models/clnModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, APICallStatusEnum, SortOrderEnum, CLN_DEFAULT_PAGE_SETTINGS, CLN_PAGE_DEFS } from '../../../../../shared/services/consts-enums-functions';
import { ApiCallStatusPayload } from '../../../../../shared/models/apiCallsPayload';
import { LoggerService } from '../../../../../shared/services/logger.service';
import { CommonService } from '../../../../../shared/services/common.service';
import { openAlert } from '../../../../../store/rtl.actions';
import { RTLState } from '../../../../../store/rtl.state';
import { clnPageSettings, channels } from '../../../../store/cln.selector';
import { ColumnDefinition, PageSettings, TableSetting } from '../../../../../shared/models/pageSettings';
import { CamelCaseWithReplacePipe } from '../../../../../shared/pipes/app.pipe';
import { MAT_SELECT_CONFIG } from '@angular/material/select';
@Component({
selector: 'rtl-cln-channel-active-htlcs-table',
templateUrl: './channel-active-htlcs-table.component.html',
styleUrls: ['./channel-active-htlcs-table.component.scss'],
providers: [
{ provide: MAT_SELECT_CONFIG, useValue: { overlayPanelClass: 'rtl-select-overlay' } },
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('HTLCs') }
]
})
export class CLNChannelActiveHTLCsTableComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | undefined;
public nodePageDefs = CLN_PAGE_DEFS;
public selFilterBy = 'all';
public colWidth = '20rem';
public PAGE_ID = 'peers_channels';
public tableSetting: TableSetting = { tableId: 'active_HTLCs', recordsPerPage: PAGE_SIZE, sortBy: 'expiry', sortOrder: SortOrderEnum.DESCENDING };
public channels: any = new MatTableDataSource([]);
public channelsJSONArr: Channel[] = [];
public displayedColumns: any[] = [];
public htlcColumns = [];
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
public errorMessage = '';
public selFilter = '';
public apiCallStatus: ApiCallStatusPayload | null = null;
public apiCallStatusEnum = APICallStatusEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<RTLState>, private camelCaseWithReplace: CamelCaseWithReplacePipe) {
this.screenSize = this.commonService.getScreenSize();
}
ngOnInit() {
this.store.select(clnPageSettings).pipe(takeUntil(this.unSubs[0])).
subscribe((settings: { pageSettings: PageSettings[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = settings.apiCallStatus;
if (this.apiCallStatus.status === APICallStatusEnum.ERROR) {
this.errorMessage = this.apiCallStatus.message || '';
}
this.tableSetting = settings.pageSettings.find((page) => page.pageId === this.PAGE_ID)?.tables.find((table) => table.tableId === this.tableSetting.tableId) || CLN_DEFAULT_PAGE_SETTINGS.find((page) => page.pageId === this.PAGE_ID)?.tables.find((table) => table.tableId === this.tableSetting.tableId)!;
if (this.screenSize === ScreenSizeEnum.XS || this.screenSize === ScreenSizeEnum.SM) {
this.displayedColumns = JSON.parse(JSON.stringify(this.tableSetting.columnSelectionSM));
} else {
this.displayedColumns = JSON.parse(JSON.stringify(this.tableSetting.columnSelection));
}
this.displayedColumns.push('actions');
this.pageSize = this.tableSetting.recordsPerPage ? +this.tableSetting.recordsPerPage : PAGE_SIZE;
this.colWidth = this.displayedColumns.length ? ((this.commonService.getContainerSize().width / this.displayedColumns.length) / 14) + 'rem' : '20rem';
this.logger.info(this.displayedColumns);
});
this.store.select(channels).pipe(takeUntil(this.unSubs[1])).
subscribe((channelsSelector: { activeChannels: Channel[], pendingChannels: Channel[], inactiveChannels: Channel[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = channelsSelector.apiCallStatus;
if (this.apiCallStatus.status === APICallStatusEnum.ERROR) {
this.errorMessage = !this.apiCallStatus.message ? '' : (typeof (this.apiCallStatus.message) === 'object') ? JSON.stringify(this.apiCallStatus.message) : this.apiCallStatus.message;
}
const allChannels = [...channelsSelector.activeChannels, ...channelsSelector.pendingChannels, ...channelsSelector.inactiveChannels];
this.channelsJSONArr = allChannels?.filter((channel) => channel.htlcs && channel.htlcs.length > 0) || [];
if (this.channelsJSONArr.length > 0 && this.sort && this.paginator && this.displayedColumns.length > 0) {
this.loadHTLCsTable(this.channelsJSONArr);
}
this.logger.info(channelsSelector);
});
}
ngAfterViewInit() {
if (this.channelsJSONArr.length > 0) {
this.loadHTLCsTable(this.channelsJSONArr);
}
}
onHTLCClick(selHtlc: ChannelHTLC, selChannel: Channel) {
const reorderedHTLC = [
[{ key: 'alias', value: selChannel.alias, title: 'Alias', width: 100, type: DataTypeEnum.STRING }],
[{ key: 'amount_msat', value: ((selHtlc.amount_msat || 0) / 1000), title: 'Amount (Sats)', width: 50, type: DataTypeEnum.NUMBER },
{ key: 'direction', value: this.commonService.titleCase(selHtlc.direction || ''), title: 'Direction', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'expiry', value: selHtlc.expiry, title: 'Expiry', width: 50, type: DataTypeEnum.NUMBER },
{ key: 'state', value: this.camelCaseWithReplace.transform(selHtlc.state || '', '_'), title: 'State', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'id', value: selHtlc.id, title: 'HTLC ID', width: 50, type: DataTypeEnum.STRING },
{ key: 'local_trimmed', value: selHtlc.local_trimmed, title: 'Local Trimmed', width: 50, type: DataTypeEnum.BOOLEAN }],
[{ key: 'payment_hash', value: selHtlc.payment_hash, title: 'Payment Hash', width: 100, type: DataTypeEnum.STRING }]
];
this.store.dispatch(openAlert({
payload: {
data: {
type: AlertTypeEnum.INFORMATION,
alertTitle: 'HTLC Information',
message: reorderedHTLC
}
}
}));
}
applyFilter() {
this.channels.filter = this.selFilter.trim().toLowerCase();
}
getLabel(column: string) {
const returnColumn: ColumnDefinition = this.nodePageDefs[this.PAGE_ID][this.tableSetting.tableId].allowedColumns.find((col) => col.column === column);
return returnColumn ? returnColumn.label ? returnColumn.label : this.camelCaseWithReplace.transform(returnColumn.column, '_') : this.commonService.titleCase(column);
}
setFilterPredicate() {
this.channels.filterPredicate = (rowData: Channel, fltr: string) => {
let rowToFilter = '';
switch (this.selFilterBy) {
case 'all':
rowToFilter = (rowData.alias ? rowData.alias.toLowerCase() : '') +
rowData.htlcs?.map((htlc) => JSON.stringify(htlc).toLowerCase() + (htlc.local_trimmed ? ' yes ' : ' no '));
break;
case 'direction':
rowToFilter = rowData.htlcs?.map((htlc) => htlc.direction + ' ').toString() || '';
break;
case 'id':
rowToFilter = rowData.htlcs?.map((htlc) => htlc.id + ' ').toString() || '';
break;
case 'expiry':
rowToFilter = rowData.htlcs?.map((htlc) => htlc.expiry + ' ').toString() || '';
break;
case 'state':
rowToFilter = rowData.htlcs?.map((htlc) => this.camelCaseWithReplace.transform(htlc.state || '', '_').toLowerCase() + ' ').toString() || '';
break;
case 'payment_hash':
rowToFilter = rowData.htlcs?.map((htlc) => htlc.payment_hash + ' ').toString() || '';
break;
case 'local_trimmed':
rowToFilter = rowData.htlcs?.map((htlc) => (htlc.local_trimmed ? ' yes ' : ' no ')).toString() || '';
break;
case 'amount_msat':
rowToFilter = (rowData.htlcs?.map((htlc) => (htlc.amount_msat || 0) / 1000))?.toString() || '';
break;
default:
rowToFilter = typeof rowData[this.selFilterBy] === 'undefined' ? '' : typeof rowData[this.selFilterBy] === 'string' ? rowData[this.selFilterBy].toLowerCase() : typeof rowData[this.selFilterBy] === 'boolean' ? (rowData[this.selFilterBy] ? 'yes' : 'no') : rowData[this.selFilterBy].toString();
break;
}
return rowToFilter.includes(fltr);
};
}
loadHTLCsTable(channels: Channel[]) {
this.channels = (channels) ? new MatTableDataSource<Channel>([...channels]) : new MatTableDataSource([]);
this.channels.sort = this.sort;
this.channels.sortingDataAccessor = (data: any, sortHeaderId: string) => {
switch (sortHeaderId) {
case 'amount_msat':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'number', this.sort?.direction);
return data.htlcs && data.htlcs.length ? data.htlcs.length : null;
case 'id':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'string', this.sort?.direction);
return data;
case 'direction':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'string', this.sort?.direction);
return data.alias ? data.alias : data.id ? data.id : null;
case 'expiry':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'number', this.sort?.direction);
return data;
case 'payment_hash':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'string', this.sort?.direction);
return data;
case 'state':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'string', this.sort?.direction);
return data;
case 'local_trimmed':
this.commonService.sortByKey(data.htlcs, sortHeaderId, 'boolean', this.sort?.direction);
return data;
default:
return (data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null;
}
};
this.channels.paginator = this.paginator;
this.setFilterPredicate();
this.applyFilter();
}
onDownloadCSV() {
if (this.channels.data && this.channels.data.length > 0) {
this.commonService.downloadFile(this.flattenHTLCs(), 'ActiveHTLCs');
}
}
flattenHTLCs() {
const channelsDataCopy = JSON.parse(JSON.stringify(this.channels.data));
const flattenedHTLCs = channelsDataCopy?.reduce((acc, curr) => {
if (curr.htlcs) {
return acc.concat(curr.htlcs);
} else {
return acc.concat(curr);
}
}, []);
return flattenedHTLCs;
}
ngOnDestroy() {
this.unSubs.forEach((completeSub) => {
completeSub.next(<any>null);
completeSub.complete();
});
}
}

@ -14,6 +14,11 @@
<span matBadgeOverlap="false" class="tab-badge" matBadge="{{pendingChannels}}">Pending/Inactive</span>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<span matBadgeOverlap="false" class="tab-badge" matBadge="{{activeHTLCs}}">Active HTLCs</span>
</ng-template>
</mat-tab>
</mat-tab-group>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-x-large">
<router-outlet></router-outlet>

@ -24,12 +24,13 @@ export class CLNChannelsTablesComponent implements OnInit, OnDestroy {
public openChannels = 0;
public pendingChannels = 0;
public activeHTLCs = 0;
public selNode: SelNodeChild | null = {};
public information: GetInfo = {};
public peers: Peer[] = [];
public utxos: UTXO[] = [];
public totalBalance = 0;
public links = [{ link: 'open', name: 'Open' }, { link: 'pending', name: 'Pending/Inactive' }];
public links = [{ link: 'open', name: 'Open' }, { link: 'pending', name: 'Pending/Inactive' }, { link: 'activehtlcs', name: 'Active HTLCs' }];
public activeLink = 0;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
@ -51,18 +52,20 @@ export class CLNChannelsTablesComponent implements OnInit, OnDestroy {
this.logger.info(infoSettingsBalSelector);
});
this.store.select(peers).pipe(takeUntil(this.unSubs[2])).
subscribe((peersSeletor: { peers: Peer[], apiCallStatus: ApiCallStatusPayload }) => {
this.peers = peersSeletor.peers;
subscribe((peersSelector: { peers: Peer[], apiCallStatus: ApiCallStatusPayload }) => {
this.peers = peersSelector.peers;
});
this.store.select(utxos).pipe(takeUntil(this.unSubs[3])).
subscribe((utxosSeletor: { utxos: UTXO[], apiCallStatus: ApiCallStatusPayload }) => {
this.utxos = this.commonService.sortAscByKey(utxosSeletor.utxos?.filter((utxo) => utxo.status === 'confirmed'), 'value');
});
this.store.select(channels).pipe(takeUntil(this.unSubs[4])).
subscribe((channelsSeletor: { activeChannels: Channel[], pendingChannels: Channel[], inactiveChannels: Channel[], apiCallStatus: ApiCallStatusPayload }) => {
this.openChannels = channelsSeletor.activeChannels.length || 0;
this.pendingChannels = (channelsSeletor.pendingChannels.length + channelsSeletor.inactiveChannels.length) || 0;
this.logger.info(channelsSeletor);
subscribe((channelsSelector: { activeChannels: Channel[], pendingChannels: Channel[], inactiveChannels: Channel[], apiCallStatus: ApiCallStatusPayload }) => {
this.openChannels = channelsSelector.activeChannels.length || 0;
this.pendingChannels = (channelsSelector.pendingChannels.length + channelsSelector.inactiveChannels.length) || 0;
const allChannels = [...channelsSelector.activeChannels, ...channelsSelector.pendingChannels, ...channelsSelector.inactiveChannels];
this.activeHTLCs = allChannels?.reduce((totalHTLCs, peer) => totalHTLCs + (peer.htlcs && peer.htlcs.length > 0 ? peer.htlcs.length : 0), 0);
this.logger.info(channelsSelector);
});
}

@ -28,7 +28,7 @@ export class ECLChannelRebalanceComponent implements OnInit, OnDestroy {
public selChannel: Channel = {};
public activeChannels: Channel[] = [];
public filteredActiveChannels: Observable<Channel[]>;
public rebalanceStatus: { flgReusingInvoice: boolean, invoice: string, paymentHash: string, paymentDetails: any, paymentStatus: any } =
public rebalanceStatus: { flgReusingInvoice: boolean, invoice: string, paymentHash: string, paymentDetails: any, paymentStatus: any } =
{ flgReusingInvoice: false, invoice: '', paymentHash: '', paymentDetails: null, paymentStatus: null };
public inputFormLabel = 'Amount to rebalance';
public flgEditable = true;
@ -106,26 +106,26 @@ export class ECLChannelRebalanceComponent implements OnInit, OnDestroy {
if (!this.inputFormGroup.controls.rebalanceAmount.value || this.inputFormGroup.controls.rebalanceAmount.value <= 0 ||
(this.selChannel.toLocal && this.inputFormGroup.controls.rebalanceAmount.value > +this.selChannel.toLocal) ||
!this.inputFormGroup.controls.selRebalancePeer.value.nodeId) {
if (!this.inputFormGroup.controls.selRebalancePeer.value.nodeId) {
this.inputFormGroup.controls.selRebalancePeer.setErrors({required: true});
}
if (!this.inputFormGroup.controls.selRebalancePeer.value.nodeId) {
this.inputFormGroup.controls.selRebalancePeer.setErrors({ required: true });
}
return true;
}
this.stepper.next();
this.flgEditable = false;
this.rebalanceStatus = { flgReusingInvoice: false, invoice: '', paymentHash: '', paymentDetails: null, paymentStatus: null };
this.dataService.circularRebalance((this.inputFormGroup.controls.rebalanceAmount.value * 1000), this.selChannel.shortChannelId, this.selChannel.nodeId, this.inputFormGroup.controls.selRebalancePeer.value.shortChannelId, this.inputFormGroup.controls.selRebalancePeer.value.nodeId, [this.information.nodeId || '']).
pipe(takeUntil(this.unSubs[2])).subscribe({
next: (rebalanceRes) => {
this.logger.info(rebalanceRes);
this.rebalanceStatus = rebalanceRes;
this.flgEditable = true;
}, error: (error) => {
this.logger.error(error);
this.rebalanceStatus = error;
this.flgEditable = true;
}
});
this.dataService.circularRebalance((this.inputFormGroup.controls.rebalanceAmount.value * 1000), this.selChannel.shortChannelId, this.selChannel.nodeId, this.inputFormGroup.controls.selRebalancePeer.value.shortChannelId, this.inputFormGroup.controls.selRebalancePeer.value.nodeId, [this.information.nodeId || '']).
pipe(takeUntil(this.unSubs[2])).subscribe({
next: (rebalanceRes) => {
this.logger.info(rebalanceRes);
this.rebalanceStatus = rebalanceRes;
this.flgEditable = true;
}, error: (error) => {
this.logger.error(error);
this.rebalanceStatus = error;
this.flgEditable = true;
}
});
}
filterActiveChannels() {

@ -297,6 +297,16 @@ export interface QueryRoutes {
routes: Routes[];
}
export interface ChannelHTLC {
direction?: string;
id?: string;
amount_msat?: number;
expiry?: number;
payment_hash?: string;
state?: string;
local_trimmed?: boolean;
}
export interface Channel {
id?: string;
alias?: string;
@ -313,6 +323,7 @@ export interface Channel {
our_channel_reserve_satoshis?: string;
spendable_msatoshi?: string;
direction?: number;
htlcs?: ChannelHTLC[];
balancedness?: number; // Between 0-1-0
}

@ -113,6 +113,7 @@ export class CLNPageDefinitions {
open_channels: TableDefinition;
pending_inactive_channels: TableDefinition;
peers: TableDefinition;
active_HTLCs: TableDefinition;
};
liquidity_ads: {
liquidity_ads: TableDefinition;

@ -737,7 +737,10 @@ export const CLN_DEFAULT_PAGE_SETTINGS: PageSettings[] = [
columnSelection: ['alias', 'connected', 'state', 'msatoshi_total'] },
{ tableId: 'peers', recordsPerPage: PAGE_SIZE, sortBy: 'alias', sortOrder: SortOrderEnum.ASCENDING,
columnSelectionSM: ['alias', 'id'],
columnSelection: ['alias', 'id', 'netaddr'] }
columnSelection: ['alias', 'id', 'netaddr'] },
{ tableId: 'active_HTLCs', recordsPerPage: PAGE_SIZE, sortBy: 'expiry', sortOrder: SortOrderEnum.DESCENDING,
columnSelectionSM: ['amount_msat', 'direction', 'expiry'],
columnSelection: ['amount_msat', 'direction', 'expiry', 'state'] }
] },
{ pageId: 'liquidity_ads', tables: [
{ tableId: 'liquidity_ads', recordsPerPage: PAGE_SIZE, sortBy: 'channel_opening_fee', sortOrder: SortOrderEnum.ASCENDING,
@ -821,6 +824,11 @@ export const CLN_PAGE_DEFS: CLNPageDefinitions = {
peers: {
maxColumns: 3,
allowedColumns: [{ column:'alias' }, { column:'id' }, { column:'netaddr', label: 'Network Address' }]
},
active_HTLCs: {
maxColumns: 7,
allowedColumns: [{ column:'amount_msat', label: 'Amount (Sats)' }, { column:'direction' }, { column:'id', label: 'HTLC ID' }, { column:'state' },
{ column:'expiry' }, { column:'payment_hash' }, { column:'local_trimmed' }]
}
},
liquidity_ads: {

Loading…
Cancel
Save