Channel Rebalance UX

Channel Rebalance UX
pull/266/head
Shahana Farooqui 4 years ago
parent 76c07fc90c
commit 93dc8b4d26

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

@ -9,8 +9,8 @@
<link rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon/favicon-16x16.png">
<link rel="manifest" href="assets/images/favicon/site.webmanifest"> <link rel="manifest" href="assets/images/favicon/site.webmanifest">
<link rel="stylesheet" href="styles.492de97a07406803820c.css"></head> <link rel="stylesheet" href="styles.005cfaa29a6270e890a9.css"></head>
<body> <body>
<rtl-app></rtl-app> <rtl-app></rtl-app>
<script src="runtime.0bf5b54cde6771714681.js" defer></script><script src="polyfills-es5.2ae7ace69949ec0a3f00.js" nomodule defer></script><script src="polyfills.3302e98effc5e50a54c2.js" defer></script><script src="main.1af7e43e36e8af5de7fa.js" defer></script></body> <script src="runtime.bc7e3c8913fcb3cca951.js" defer></script><script src="polyfills-es5.2ae7ace69949ec0a3f00.js" nomodule defer></script><script src="polyfills.3302e98effc5e50a54c2.js" defer></script><script src="main.cb823a6e983a6812fd66.js" defer></script></body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"4f843475d2c07c80c8fb",6:"c1ed42620daa44404ae5",7:"091c7a4253ca69959683"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]); !function(e){function r(r){for(var n,a,i=r[0],c=r[1],f=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(l&&l(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"a69b5e7729a9d343f7d1",6:"0fc217575a57adec8f25",7:"756b75454a1b6fca6a49"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(f);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var f=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var f=0;f<i.length;f++)r(i[f]);var l=c;t()}([]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -120,6 +120,9 @@ exports.getGraphEdge = (req, res, next) => {
exports.getQueryRoutes = (req, res, next) => { exports.getQueryRoutes = (req, res, next) => {
options = common.getOptions(); options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/graph/routes/' + req.params.destPubkey + '/' + req.params.amount; options.url = common.getSelLNServerUrl() + '/graph/routes/' + req.params.destPubkey + '/' + req.params.amount;
if(req.query.outgoingChanId) {
options.url = options.url + '?outgoing_chan_id=' + req.query.outgoingChanId;
}
request(options).then((body) => { request(options).then((body) => {
logger.info({fileName: 'Graph', msg: 'Query Routes Received: ' + JSON.stringify(body)}); logger.info({fileName: 'Graph', msg: 'Query Routes Received: ' + JSON.stringify(body)});
if(undefined === body || body.error) { if(undefined === body || body.error) {

@ -195,7 +195,6 @@ export class LNDEffects implements OnDestroy {
.pipe( .pipe(
map((postRes: any) => { map((postRes: any) => {
this.logger.info(postRes); this.logger.info(postRes);
this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: action.payload.pageSize, reversed: true })); this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: action.payload.pageSize, reversed: true }));
postRes.memo = action.payload.memo; postRes.memo = action.payload.memo;
postRes.value = action.payload.invoiceValue; postRes.value = action.payload.invoiceValue;
@ -205,6 +204,7 @@ export class LNDEffects implements OnDestroy {
postRes.creation_date = Math.round(new Date().getTime() / 1000).toString(); postRes.creation_date = Math.round(new Date().getTime() / 1000).toString();
postRes.creation_date_str = this.commonService.convertTimestampToDate(+postRes.creation_date); postRes.creation_date_str = this.commonService.convertTimestampToDate(+postRes.creation_date);
if (action.payload.openModal) { if (action.payload.openModal) {
this.store.dispatch(new RTLActions.CloseSpinner());
return { return {
type: RTLActions.OPEN_ALERT, type: RTLActions.OPEN_ALERT,
payload: { data: { payload: { data: {
@ -660,7 +660,7 @@ export class LNDEffects implements OnDestroy {
mergeMap(([action, store]: [RTLActions.SendPayment, any]) => { mergeMap(([action, store]: [RTLActions.SendPayment, any]) => {
let queryHeaders = {}; let queryHeaders = {};
if (action.payload.outgoingChannel) { queryHeaders['outgoingChannel'] = action.payload.outgoingChannel.chan_id; } if (action.payload.outgoingChannel) { queryHeaders['outgoingChannel'] = action.payload.outgoingChannel.chan_id; }
if (action.payload.allowSelfPayment) { queryHeaders['allowSelfPayment'] = action.payload.allowSelfPayment; } if (action.payload.allowSelfPayment) { queryHeaders['allowSelfPayment'] = action.payload.allowSelfPayment; } // Channel Rebalancing
if (action.payload.lastHopPubkey) { queryHeaders['lastHopPubkey'] = action.payload.lastHopPubkey; } if (action.payload.lastHopPubkey) { queryHeaders['lastHopPubkey'] = action.payload.lastHopPubkey; }
if(action.payload.feeLimitType && action.payload.feeLimitType !== FEE_LIMIT_TYPES[0]) { if(action.payload.feeLimitType && action.payload.feeLimitType !== FEE_LIMIT_TYPES[0]) {
queryHeaders['feeLimit'] = {}; queryHeaders['feeLimit'] = {};
@ -675,34 +675,52 @@ export class LNDEffects implements OnDestroy {
.pipe( .pipe(
map((sendRes: any) => { map((sendRes: any) => {
this.logger.info(sendRes); this.logger.info(sendRes);
this.store.dispatch(new RTLActions.CloseSpinner());
if (sendRes.payment_error) { if (sendRes.payment_error) {
this.logger.error('Error: ' + sendRes.payment_error); if (action.payload.allowSelfPayment) {
const myErr = {status: sendRes.payment_error.status, error: sendRes.payment_error.error.message}; this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: PAGE_SIZE, reversed: true }));
this.handleErrorWithAlert('ERROR', 'Send Payment Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/transactions/' + action.payload.paymentReq, myErr); return {
return of({type: RTLActions.VOID}); type: RTLActions.SEND_PAYMENT_STATUS,
} else { payload: sendRes
let msg = 'Payment Sent Successfully.'; };
if(sendRes.payment_route && sendRes.payment_route.total_fees_msat) { } else {
msg = 'Payment sent successfully with the total fee ' + sendRes.payment_route.total_fees_msat + ' (mSats).'; this.logger.error('Error: ' + sendRes.payment_error);
const myErr = {status: sendRes.payment_error.status, error: sendRes.payment_error.error.message};
this.handleErrorWithAlert('ERROR', 'Send Payment Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/transactions/' + action.payload.paymentReq, myErr);
return of({type: RTLActions.VOID});
} }
this.store.dispatch(new RTLActions.OpenSnackBar(msg)); } else {
this.store.dispatch(new RTLActions.SetDecodedPayment({}));
this.store.dispatch(new RTLActions.FetchAllChannels()); this.store.dispatch(new RTLActions.FetchAllChannels());
this.store.dispatch(new RTLActions.FetchBalance('channels')); this.store.dispatch(new RTLActions.FetchBalance('channels'));
this.store.dispatch(new RTLActions.FetchPayments()); this.store.dispatch(new RTLActions.FetchPayments());
if (action.payload.allowSelfPayment) { if (action.payload.allowSelfPayment) {
this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: PAGE_SIZE, reversed: true })); this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: PAGE_SIZE, reversed: true }));
} else {
this.store.dispatch(new RTLActions.CloseSpinner());
let msg = 'Payment Sent Successfully.';
if(sendRes.payment_route && sendRes.payment_route.total_fees_msat) {
msg = 'Payment sent successfully with the total fee ' + sendRes.payment_route.total_fees_msat + ' (mSats).';
}
this.store.dispatch(new RTLActions.OpenSnackBar(msg));
} }
return { return {
type: RTLActions.SET_DECODED_PAYMENT, type: RTLActions.SEND_PAYMENT_STATUS,
payload: {} payload: sendRes
}; };
} }
}), }),
catchError((err: any) => { catchError((err: any) => {
const myErr = {status: err.status, error: err.error && err.error.error && typeof(err.error.error) === 'object' ? err.error.error : {error: err.error && err.error.error ? err.error.error : 'Unknown Error'}}; if (action.payload.allowSelfPayment) {
this.handleErrorWithAlert('ERROR', 'Send Payment Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/transactions', myErr); this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: PAGE_SIZE, reversed: true }));
return of({type: RTLActions.VOID}); return of({
type: RTLActions.SEND_PAYMENT_STATUS,
payload: err
});
} else {
const myErr = {status: err.status, error: err.error && err.error.error && typeof(err.error.error) === 'object' ? err.error.error : {error: err.error && err.error.error ? err.error.error : 'Unknown Error'}};
this.handleErrorWithAlert('ERROR', 'Send Payment Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/transactions', myErr);
return of({type: RTLActions.VOID});
}
}) })
); );
}) })
@ -822,7 +840,9 @@ export class LNDEffects implements OnDestroy {
queryRoutesFetch = this.actions$.pipe( queryRoutesFetch = this.actions$.pipe(
ofType(RTLActions.GET_QUERY_ROUTES), ofType(RTLActions.GET_QUERY_ROUTES),
mergeMap((action: RTLActions.GetQueryRoutes) => { mergeMap((action: RTLActions.GetQueryRoutes) => {
return this.httpClient.get(this.CHILD_API_URL + environment.NETWORK_API + '/routes/' + action.payload.destPubkey + '/' + action.payload.amount) let url = this.CHILD_API_URL + environment.NETWORK_API + '/routes/' + action.payload.destPubkey + '/' + action.payload.amount;
if (action.payload.outgoingChanId) { url = url + '?outgoing_chan_id=' + action.payload.outgoingChanId; }
return this.httpClient.get(url)
.pipe( .pipe(
map((qrRes: any) => { map((qrRes: any) => {
this.logger.info(qrRes); this.logger.info(qrRes);

@ -2,45 +2,130 @@
<div fxFlex="100" class="padding-gap-large"> <div fxFlex="100" class="padding-gap-large">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between center" class="modal-info-header"> <mat-card-header fxLayout="row" fxLayoutAlign="space-between center" class="modal-info-header">
<div fxFlex="95" fxLayoutAlign="start start"><span class="page-title">Channel Rebalance</span></div> <div fxFlex="95" fxLayoutAlign="start start"><span class="page-title">Channel Rebalance</span></div>
<button tabindex="7" fxFlex="5" fxLayoutAlign="center" class="btn-close-x p-0" (click)="onClose()" mat-button>X</button> <button tabindex="15" fxFlex="5" fxLayoutAlign="center" class="btn-close-x p-0" (click)="onClose()" mat-button>X</button>
</mat-card-header> </mat-card-header>
<mat-card-content class="mt-5px"> <mat-card-content class="mt-5px">
<form fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign="space-between stretch" #form="ngForm"> <div fxLayout="column" class="bordered-box p-2">
<div fxLayout="column" class="bordered-box p-2"> <p fxLayoutAlign="start center" class="pb-1">Rebalancing for Channel: {{selChannel.chan_id}}</p>
<p fxLayoutAlign="start center" class="pb-1">Rebalancing for Channel: {{selChannel.chan_id}}</p> <mat-vertical-stepper [linear]="true" #stepper>
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch"> <mat-step [stepControl]="inputFormGroup">
<mat-form-field fxFlex="30"> <form [formGroup]="inputFormGroup" fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
<input autoFocus matInput [(ngModel)]="rebalanceAmount" (change)="filterActiveChannels()" placeholder="Amount" type="number" step="100" min="1" tabindex="1" required name="amount" #amount="ngModel" max="{{selChannel?.local_balance}}"> <ng-template matStepLabel>Enter info to rebalance</ng-template>
<mat-hint>(Local Bal: {{selChannel?.local_balance}}, Remaining: {{selChannel?.local_balance - ((rebalanceAmount) ? rebalanceAmount : 0)}})</mat-hint> <div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<span matSuffix>Sats</span> <mat-form-field fxFlex="48">
<mat-error *ngIf="!rebalanceAmount">Amount is required.</mat-error> <input autoFocus matInput (change)="filterActiveChannels()" placeholder="Amount" type="number" step="100" tabindex="1" formControlName="rebalanceAmount" required>
<mat-error *ngIf="amount.errors?.max">Amount must be less than or equal to {{selChannel?.local_balance}}.</mat-error> <mat-hint>(Local Bal: {{selChannel?.local_balance}}, Remaining: {{selChannel?.local_balance - ((inputFormGroup.controls.rebalanceAmount.value) ? inputFormGroup.controls.rebalanceAmount.value : 0)}})</mat-hint>
</mat-form-field> <span matSuffix>Sats</span>
<mat-form-field fxFlex="30" fxLayoutAlign="start end"> <mat-error *ngIf="inputFormGroup.controls.rebalanceAmount.errors?.required">Amount is required.</mat-error>
<mat-select tabindex="2" [(value)]="selRebalancePeer" placeholder="Receive from Peer"> <mat-error *ngIf="inputFormGroup.controls.rebalanceAmount.errors?.min">Amount must be a positive number.</mat-error>
<mat-option *ngFor="let activeChannel of activeChannels" [value]="activeChannel"> <mat-error *ngIf="inputFormGroup.controls.rebalanceAmount.errors?.max">Amount must be less than or equal to {{selChannel?.local_balance}}.</mat-error>
{{activeChannel.remote_alias || activeChannel.chan_id}} </mat-form-field>
</mat-option> <mat-form-field fxFlex="48" fxLayoutAlign="start end">
</mat-select> <mat-select tabindex="2" formControlName="selRebalancePeer" placeholder="Receive from Peer" required>
</mat-form-field> <mat-option *ngFor="let activeChannel of activeChannels" [value]="activeChannel">
<mat-form-field fxFlex="15" fxLayoutAlign="start end"> {{activeChannel.remote_alias || activeChannel.chan_id}}
<mat-select tabindex="3" [(value)]="selFeeLimitType" Placeholder="Fee Limits" required> </mat-option>
<mat-option *ngFor="let feeLimitType of feeLimitTypes" [value]="feeLimitType"> </mat-select>
{{feeLimitType.name}} </mat-form-field>
</mat-option> </div>
</mat-select> <div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
</mat-form-field> <button mat-flat-button color="primary" tabindex="3" type="button" (click)="onEstimateFee()" matStepperNext>Estimate Fee</button>
<mat-form-field fxFlex="20"> </div>
<input matInput [(ngModel)]="feeLimit" [placeholder]="selFeeLimitType?.placeholder" type="number" name="feeLimit" step="1" min="0" required tabindex="4" #feeLmt="ngModel"> </form>
<mat-error *ngIf="!feeLimit">{{selFeeLimitType?.placeholder}} is required.</mat-error> </mat-step>
</mat-form-field> <mat-step [stepControl]="feeFormGroup">
</div> <form [formGroup]="feeFormGroup" fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
</div> <ng-template matStepLabel>Select rebalance fee</ng-template>
<div class="mt-2" fxLayout="row" fxLayoutAlign="end center" fxFlex="100"> <div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<button fxFlex="48" fxFlex.gt-sm="20" fxLayoutAlign="center center" mat-stroked-button class="mr-2" color="primary" tabindex="5" type="reset" (click)="resetData()">Clear Field</button> <div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<button fxFlex="48" fxFlex.gt-sm="20" fxLayoutAlign="center center" mat-flat-button color="primary" (click)="onRebalance()" tabindex="6">Rebalance</button> <div fxFlex="75" class="alert">
</div> <fa-icon [icon]="faInfoCircle" class="mr-1 alert-icon"></fa-icon>
</form> <span>
<strong>Estimated Fee: </strong>{{queryRoute && queryRoute.routes && queryRoute.routes.length > 0 && queryRoute.routes[0].total_fees_msat ? queryRoute.routes[0].total_fees_msat : 0}} mSats |
<strong>Number of Hops: </strong>{{queryRoute && queryRoute.routes && queryRoute.routes.length > 0 && queryRoute.routes[0].hops && queryRoute.routes[0].hops.length ? queryRoute.routes[0].hops.length : 0}}
</span>
</div>
<button mat-stroked-button type="button" tabindex="4" (click)="onEstimateFee()" class="h-35px" matTooltip="Estimate Fee"><mat-icon class="mb-5px">loop</mat-icon></button>
<button fxFlex="15" mat-stroked-button type="button" tabindex="5" class="h-35px" (click)="onUseEstimate()">Use Estimate</button>
</div>
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<mat-form-field fxFlex="48" fxLayoutAlign="start end">
<mat-select tabindex="6" formControlName="selFeeLimitType" Placeholder="Fee Limits" required>
<mat-option *ngFor="let feeLimitType of feeLimitTypes" [value]="feeLimitType">
{{feeLimitType.name}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="48">
<input matInput formControlName="feeLimit" placeholder="{{feeFormGroup.controls.selFeeLimitType.value ? feeFormGroup.controls.selFeeLimitType.value.placeholder : feeLimitTypes[0].placeholder}}" type="number" step="1" tabindex="7" required>
<mat-error *ngIf="feeFormGroup.controls.feeLimit.errors?.required">{{feeFormGroup.controls.selFeeLimitType.value ? feeFormGroup.controls.selFeeLimitType.value.placeholder : feeLimitTypes[0].placeholder}} is required.</mat-error>
<mat-error *ngIf="feeFormGroup.controls.feeLimit.errors?.min">{{feeFormGroup.controls.selFeeLimitType.value ? feeFormGroup.controls.selFeeLimitType.value.placeholder : feeLimitTypes[0].placeholder}} must be a positive number.</mat-error>
</mat-form-field>
</div>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button mat-flat-button color="primary" tabindex="8" type="button" (click)="onRebalance()" matStepperNext>Rebalance</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="statusFormGroup">
<form [formGroup]="statusFormGroup" fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
<ng-template matStepLabel>Invoice/Payment</ng-template>
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<mat-progress-bar fxFlex="100" *ngIf="!flgInvoiceGenerated" color="primary" mode="indeterminate"></mat-progress-bar>
<mat-expansion-panel class="flat-expansion-panel mb-2" fxFlex="100">
<mat-expansion-panel-header>
<mat-panel-title>
<span fxLayoutAlign="start center" fxFlex="100">{{flgInvoiceGenerated ? 'Invoice generated' : 'Generating invoice...'}}<mat-icon *ngIf="flgInvoiceGenerated" class="ml-1 icon-small">check</mat-icon></span>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column"><span class="foreground-secondary-text">{{paymentRequest}}</span></div>
</mat-expansion-panel>
<mat-progress-bar fxFlex="100" *ngIf="flgInvoiceGenerated && !flgPaymentSent" color="primary" mode="indeterminate"></mat-progress-bar>
<mat-expansion-panel class="flat-expansion-panel" fxFlex="100">
<mat-expansion-panel-header>
<mat-panel-title>
<span fxLayoutAlign="start center" fxFlex="100">{{!flgInvoiceGenerated && !flgPaymentSent ? 'Payment waiting for Invoice' : (!flgPaymentSent ? 'Processing payment...' : (paymentStatus?.error ? 'Payment failed' : 'Payment and Rebalancing successful'))}}<mat-icon *ngIf="flgPaymentSent" class="ml-1 icon-small">{{paymentStatus?.error ? 'close' : 'check'}}</mat-icon></span>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column" *ngIf="!paymentStatus; else paymentStatusBlock"></div>
</mat-expansion-panel>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button mat-stroked-button color="primary" type="button" (click)="stepper.reset()" class="mr-1">Clear Field</button>
<button mat-flat-button color="primary" type="button" [mat-dialog-close]="false" default>Close</button>
</div>
</form>
</mat-step>
</mat-vertical-stepper>
</div>
</mat-card-content> </mat-card-content>
</div> </div>
</div> </div>
<ng-template #paymentStatusBlock>
<ng-container *ngTemplateOutlet="paymentStatus.error ? paymentFailedBlock : paymentSuccessfulBlock"></ng-container>
</ng-template>
<ng-template #paymentFailedBlock>
<div fxLayout="column"><span class="foreground-secondary-text">{{'Error: ' + (paymentStatus.error.error ? paymentStatus.error.error : paymentStatus.error ? paymentStatus.error : 'Unknown')}}</span></div>
</ng-template>
<ng-template #paymentSuccessfulBlock>
<div fxLayout="column">
<div fxLayout="row">
<div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">Payment Hash</h4>
<span class="foreground-secondary-text">{{paymentStatus.payment_hash}}</span>
</div>
</div>
<mat-divider class="w-100 my-1"></mat-divider>
<div fxLayout="row">
<div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Total Fees ({{paymentStatus.payment_route.total_fees_msat ? 'mSats' : 'Sats'}})</h4>
<span class="foreground-secondary-text">{{paymentStatus.payment_route.total_fees_msat ? paymentStatus.payment_route.total_fees_msat : paymentStatus.payment_route.total_fees ? paymentStatus.payment_route.total_fees : 0}}</span>
</div>
<div fxFlex="50">
<h4 fxLayoutAlign="start" class="font-bold-500">Number of Hops</h4>
<span class="foreground-secondary-text">{{paymentStatus && paymentStatus.payment_route && paymentStatus.payment_route.hops && paymentStatus.payment_route.hops.length ? paymentStatus.payment_route.hops.length : 0}}</span>
</div>
</div>
</div>
</ng-template>

@ -1,15 +1,18 @@
import { Component, OnInit, Inject, OnDestroy, ViewChild } from '@angular/core'; import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators'; import { takeUntil, filter } from 'rxjs/operators';
import { Actions } from '@ngrx/effects'; import { Actions, act } from '@ngrx/effects';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { ChannelInformation } from '../../../models/alertData'; import { ChannelInformation } from '../../../models/alertData';
import { LoggerService } from '../../../services/logger.service'; import { LoggerService } from '../../../services/logger.service';
import { Channel } from '../../../models/lndModels'; import { Channel, QueryRoutes } from '../../../models/lndModels';
import { FEE_LIMIT_TYPES, PAGE_SIZE } from '../../../services/consts-enums-functions'; import { FEE_LIMIT_TYPES, PAGE_SIZE } from '../../../services/consts-enums-functions';
import { LNDEffects } from '../../../../lnd/store/lnd.effects';
import * as fromRTLReducer from '../../../../store/rtl.reducers'; import * as fromRTLReducer from '../../../../store/rtl.reducers';
import * as RTLActions from '../../../../store/rtl.actions'; import * as RTLActions from '../../../../store/rtl.actions';
@ -19,58 +22,91 @@ import * as RTLActions from '../../../../store/rtl.actions';
styleUrls: ['./channel-rebalance.component.scss'] styleUrls: ['./channel-rebalance.component.scss']
}) })
export class ChannelRebalanceComponent implements OnInit, OnDestroy { export class ChannelRebalanceComponent implements OnInit, OnDestroy {
@ViewChild('form', { static: false }) form: any; public faInfoCircle = faInfoCircle;
public selChannel: Channel = {}; public selChannel: Channel = {};
public rebalanceAmount = null;
public selRebalancePeer: Channel = {};
public activeChannels = []; public activeChannels = [];
public feeLimit = null;
public selFeeLimitType = {id:'', name: '', placeholder: ''};
public feeLimitTypes = []; public feeLimitTypes = [];
public queryRoute: QueryRoutes = {};
public paymentRequest = '';
public paymentStatus: any = null;
public flgInvoiceGenerated = false;
public flgPaymentSent = false;
isLinear = false;
inputFormGroup: FormGroup;
feeFormGroup: FormGroup;
statusFormGroup: FormGroup;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<ChannelRebalanceComponent>, @Inject(MAT_DIALOG_DATA) public data: ChannelInformation, private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions) { } constructor(public dialogRef: MatDialogRef<ChannelRebalanceComponent>, @Inject(MAT_DIALOG_DATA) public data: ChannelInformation, private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions, private lndEffects: LNDEffects, private formBuilder: FormBuilder) { }
ngOnInit() { ngOnInit() {
this.selChannel = this.data.channel;
FEE_LIMIT_TYPES.forEach((FEE_LIMIT_TYPE, i) => { FEE_LIMIT_TYPES.forEach((FEE_LIMIT_TYPE, i) => {
if(i > 0) { if(i > 0) {
this.feeLimitTypes.push(FEE_LIMIT_TYPE); this.feeLimitTypes.push(FEE_LIMIT_TYPE);
} }
}); });
this.selFeeLimitType = this.feeLimitTypes[0]; this.inputFormGroup = this.formBuilder.group({
this.selChannel = this.data.channel; rebalanceAmount: ['', [Validators.required, Validators.min(1), Validators.max(this.selChannel.local_balance)]],
selRebalancePeer: [null, Validators.required]
});
this.feeFormGroup = this.formBuilder.group({
selFeeLimitType: [this.feeLimitTypes[0], Validators.required],
feeLimit: ['', [Validators.required, Validators.min(0)]]
});
this.statusFormGroup = this.formBuilder.group({
thirdCtrl: ['', Validators.required]
});
this.store.select('lnd') this.store.select('lnd')
.pipe(takeUntil(this.unSubs[0])) .pipe(takeUntil(this.unSubs[0]))
.subscribe((rtlStore) => { .subscribe((rtlStore) => {
this.activeChannels = rtlStore.allChannels.filter(channel => channel.active && channel.remote_balance >= this.rebalanceAmount && channel.chan_id !== this.selChannel.chan_id); this.activeChannels = rtlStore.allChannels.filter(channel => channel.active && channel.remote_balance >= this.inputFormGroup.controls.rebalanceAmount.value && channel.chan_id !== this.selChannel.chan_id);
this.logger.info(rtlStore); this.logger.info(rtlStore);
}); });
this.actions$.pipe(takeUntil(this.unSubs[1]),
filter((action) => action.type === RTLActions.SET_QUERY_ROUTES || action.type === RTLActions.SEND_PAYMENT_STATUS || action.type === RTLActions.NEWLY_SAVED_INVOICE))
.subscribe((action: (RTLActions.SetQueryRoutes | RTLActions.SendPaymentStatus | RTLActions.NewlySavedInvoice)) => {
if (action.type === RTLActions.SET_QUERY_ROUTES) { this.queryRoute = action.payload; }
if (action.type === RTLActions.SEND_PAYMENT_STATUS) {
this.logger.info(action.payload);
this.flgPaymentSent = true;
this.paymentStatus = action.payload;
}
if (action.type === RTLActions.NEWLY_SAVED_INVOICE) {
this.logger.info(action.payload);
this.flgInvoiceGenerated = true;
this.paymentRequest = action.payload.paymentRequest;
this.store.dispatch(new RTLActions.SendPayment({paymentReq: action.payload.paymentRequest, paymentDecoded: {}, zeroAmtInvoice: false, outgoingChannel: this.selChannel, feeLimitType: this.feeFormGroup.controls.selFeeLimitType.value, feeLimit: this.feeFormGroup.controls.feeLimit.value, allowSelfPayment: true, lastHopPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey}));
}
});
}
onEstimateFee() {
if(!this.inputFormGroup.controls.selRebalancePeer.value || !this.inputFormGroup.controls.rebalanceAmount.value) { return true; }
this.queryRoute = null;
this.feeFormGroup.reset();
this.feeFormGroup.controls.selFeeLimitType.setValue(this.feeLimitTypes[0]);
this.store.dispatch(new RTLActions.GetQueryRoutes({destPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey, amount: this.inputFormGroup.controls.rebalanceAmount.value, outgoingChanId: this.selChannel.chan_id}));
}
onUseEstimate() {
this.feeFormGroup.controls.selFeeLimitType.setValue(this.feeLimitTypes[0]);
this.feeFormGroup.controls.feeLimit.setValue((this.queryRoute.routes && this.queryRoute.routes.length > 0 && this.queryRoute.routes[0].total_fees) ? this.queryRoute.routes[0].total_fees : 0);
} }
onRebalance() { onRebalance() {
if (!this.rebalanceAmount || this.rebalanceAmount <= 0 || this.rebalanceAmount > this.selChannel.local_balance || !this.feeLimit || !this.selRebalancePeer) { return true; } if (!this.inputFormGroup.controls.rebalanceAmount.value || this.inputFormGroup.controls.rebalanceAmount.value <= 0 || this.inputFormGroup.controls.rebalanceAmount.value > +this.selChannel.local_balance || this.feeFormGroup.controls.feeLimit.value < 0 || !this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey) { return true; }
this.store.dispatch(new RTLActions.OpenSpinner('Creating Invoice to Rebalance...')); this.paymentRequest = '';
this.paymentStatus = null;
this.flgInvoiceGenerated = false;
this.flgPaymentSent = false;
this.store.dispatch(new RTLActions.SaveNewInvoice({ this.store.dispatch(new RTLActions.SaveNewInvoice({
memo: 'Local-Rebalance-' + this.rebalanceAmount + '-Sats', invoiceValue: this.rebalanceAmount, private: false, expiry: 3600, pageSize: PAGE_SIZE, openModal: false memo: 'Local-Rebalance-' + this.inputFormGroup.controls.rebalanceAmount.value + '-Sats', invoiceValue: this.inputFormGroup.controls.rebalanceAmount.value, private: false, expiry: 3600, pageSize: PAGE_SIZE, openModal: false
})); }));
this.actions$.pipe(takeUntil(this.unSubs[1]),
filter((action) => action.type === RTLActions.NEWLY_SAVED_INVOICE))
.subscribe((action: RTLActions.NewlySavedInvoice) => {
this.logger.info(action.payload);
this.store.dispatch(new RTLActions.OpenSpinner('Sending Payment to Rebalance...'));
this.store.dispatch(new RTLActions.SendPayment({paymentReq: action.payload.paymentRequest, paymentDecoded: {}, zeroAmtInvoice: false, outgoingChannel: this.selChannel, feeLimitType: this.selFeeLimitType, feeLimit: this.feeLimit, allowSelfPayment: true, lastHopPubkey: this.selRebalancePeer.remote_pubkey}));
this.dialogRef.close(false);
});
} }
filterActiveChannels() { filterActiveChannels() {
this.activeChannels = this.activeChannels.filter(channel => channel.remote_balance >= this.rebalanceAmount && channel.chan_id !== this.selChannel.chan_id); this.activeChannels = this.activeChannels.filter(channel => channel.remote_balance >= this.inputFormGroup.controls.rebalanceAmount.value && channel.chan_id !== this.selChannel.chan_id);
}
resetData() {
this.form.resetForm();
this.selFeeLimitType = this.feeLimitTypes[0];
this.selRebalancePeer = null;
} }
onClose() { onClose() {

@ -514,14 +514,18 @@
} }
.alert { .alert {
border: 1px solid $foreground-secondary-text;
color: $foreground-secondary-text;
background-color: $hover-background;
&.alert-info { &.alert-info {
border: 1px solid $blue-color; border: 1px solid $blue-color;
background-color: $blue-background-color; background-color: $blue-background-color;
color: $blue-color; color: $blue-color;
& .alert-icon.ng-fa-icon { & .alert-icon.ng-fa-icon {
color: $blue-color; color: $blue-color;
}
} }
}
&.alert-warn { &.alert-warn {
border: 1px solid $yellow-alert-color; border: 1px solid $yellow-alert-color;

@ -272,6 +272,10 @@ body {
margin-bottom: 2px !important; margin-bottom: 2px !important;
} }
.mb-5px {
margin-bottom: 5px !important;
}
.mb-1 { .mb-1 {
margin-bottom: 1rem !important; margin-bottom: 1rem !important;
} }
@ -759,7 +763,11 @@ body {
} }
.h-6 { .h-6 {
height: 6rem; height: 6rem !important;
}
.h-35px {
height: 3.5rem !important;
} }
a { a {
@ -973,3 +981,7 @@ table {
.border-invalid { .border-invalid {
border: 1px solid $red-color !important; border: 1px solid $red-color !important;
} }
.icon-green {
fill: $green-color;
}

@ -78,6 +78,7 @@ export const FETCH_PAYMENTS = 'FETCH_PAYMENTS';
export const SET_PAYMENTS = 'SET_PAYMENTS'; export const SET_PAYMENTS = 'SET_PAYMENTS';
export const DECODE_PAYMENT = 'DECODE_PAYMENT'; export const DECODE_PAYMENT = 'DECODE_PAYMENT';
export const SEND_PAYMENT = 'SEND_PAYMENT'; export const SEND_PAYMENT = 'SEND_PAYMENT';
export const SEND_PAYMENT_STATUS = 'SEND_PAYMENT_STATUS';
export const SET_DECODED_PAYMENT = 'SET_DECODED_PAYMENT'; export const SET_DECODED_PAYMENT = 'SET_DECODED_PAYMENT';
export const FETCH_GRAPH_NODE = 'FETCH_GRAPH_NODE'; export const FETCH_GRAPH_NODE = 'FETCH_GRAPH_NODE';
export const SET_GRAPH_NODE = 'SET_GRAPH_NODE'; export const SET_GRAPH_NODE = 'SET_GRAPH_NODE';
@ -490,6 +491,11 @@ export class SendPayment implements Action {
constructor(public payload: { paymentReq: string, paymentDecoded: PayRequest, zeroAmtInvoice: boolean, outgoingChannel?: Channel, feeLimitType?: {id: string, name: string}, feeLimit?: number, allowSelfPayment?: boolean, lastHopPubkey?: string }) {} constructor(public payload: { paymentReq: string, paymentDecoded: PayRequest, zeroAmtInvoice: boolean, outgoingChannel?: Channel, feeLimitType?: {id: string, name: string}, feeLimit?: number, allowSelfPayment?: boolean, lastHopPubkey?: string }) {}
} }
export class SendPaymentStatus implements Action {
readonly type = SEND_PAYMENT_STATUS;
constructor(public payload: any) {}
}
export class FetchGraphNode implements Action { export class FetchGraphNode implements Action {
readonly type = FETCH_GRAPH_NODE; readonly type = FETCH_GRAPH_NODE;
constructor(public payload: string) {} // payload = pubkey constructor(public payload: string) {} // payload = pubkey
@ -582,7 +588,7 @@ export class SetForwardingHistory implements Action {
export class GetQueryRoutes implements Action { export class GetQueryRoutes implements Action {
readonly type = GET_QUERY_ROUTES; readonly type = GET_QUERY_ROUTES;
constructor(public payload: {destPubkey: string, amount: number}) {} constructor(public payload: {destPubkey: string, amount: number, outgoingChanId?: string}) {}
} }
export class SetQueryRoutes implements Action { export class SetQueryRoutes implements Action {
@ -851,7 +857,7 @@ export type RTLActions =
RestoreChannels | RestoreChannelsRes | RestoreChannelsList | SetRestoreChannelsList | RestoreChannels | RestoreChannelsRes | RestoreChannelsList | SetRestoreChannelsList |
FetchTransactions | SetTransactions | FetchTransactions | SetTransactions |
FetchInvoices | SetInvoices | SetTotalInvoices | FetchInvoices | SetInvoices | SetTotalInvoices |
FetchPayments | SetPayments | SendPayment | FetchPayments | SetPayments | SendPayment | SendPaymentStatus |
DecodePayment | SetDecodedPayment | DecodePayment | SetDecodedPayment |
FetchGraphNode | SetGraphNode | GetQueryRoutes | SetQueryRoutes | FetchGraphNode | SetGraphNode | GetQueryRoutes | SetQueryRoutes |
GetNewAddress | SetNewAddress | SetChannelTransaction | GetNewAddress | SetNewAddress | SetChannelTransaction |

Loading…
Cancel
Save