Add pending htlcs to all channels tabs #1086

Add pending htlcs to all channels tabs #1086
This commit is contained in:
Shahana Farooqui 2023-05-02 17:04:43 -07:00
parent f45bbc4ad2
commit 804ba91d7b
19 changed files with 536 additions and 30 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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))})()})();

View File

@ -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))})()})();

View File

@ -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

View File

@ -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] }

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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();
});
}
}

View File

@ -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>

View File

@ -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);
});
}

View File

@ -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
}

View File

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

View File

@ -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: {