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="16x16" href="assets/images/favicon/favicon-16x16.png">
<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>
<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>

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) => {
options = common.getOptions();
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) => {
logger.info({fileName: 'Graph', msg: 'Query Routes Received: ' + JSON.stringify(body)});
if(undefined === body || body.error) {

@ -195,7 +195,6 @@ export class LNDEffects implements OnDestroy {
.pipe(
map((postRes: any) => {
this.logger.info(postRes);
this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: action.payload.pageSize, reversed: true }));
postRes.memo = action.payload.memo;
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_str = this.commonService.convertTimestampToDate(+postRes.creation_date);
if (action.payload.openModal) {
this.store.dispatch(new RTLActions.CloseSpinner());
return {
type: RTLActions.OPEN_ALERT,
payload: { data: {
@ -660,7 +660,7 @@ export class LNDEffects implements OnDestroy {
mergeMap(([action, store]: [RTLActions.SendPayment, any]) => {
let queryHeaders = {};
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.feeLimitType && action.payload.feeLimitType !== FEE_LIMIT_TYPES[0]) {
queryHeaders['feeLimit'] = {};
@ -675,34 +675,52 @@ export class LNDEffects implements OnDestroy {
.pipe(
map((sendRes: any) => {
this.logger.info(sendRes);
this.store.dispatch(new RTLActions.CloseSpinner());
if (sendRes.payment_error) {
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});
} else {
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).';
if (action.payload.allowSelfPayment) {
this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: PAGE_SIZE, reversed: true }));
return {
type: RTLActions.SEND_PAYMENT_STATUS,
payload: sendRes
};
} else {
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.FetchBalance('channels'));
this.store.dispatch(new RTLActions.FetchPayments());
if (action.payload.allowSelfPayment) {
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 {
type: RTLActions.SET_DECODED_PAYMENT,
payload: {}
type: RTLActions.SEND_PAYMENT_STATUS,
payload: sendRes
};
}
}),
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'}};
this.handleErrorWithAlert('ERROR', 'Send Payment Failed', this.CHILD_API_URL + environment.CHANNELS_API + '/transactions', myErr);
return of({type: RTLActions.VOID});
if (action.payload.allowSelfPayment) {
this.store.dispatch(new RTLActions.FetchInvoices({ num_max_invoices: PAGE_SIZE, reversed: true }));
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(
ofType(RTLActions.GET_QUERY_ROUTES),
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(
map((qrRes: any) => {
this.logger.info(qrRes);

@ -2,45 +2,130 @@
<div fxFlex="100" class="padding-gap-large">
<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>
<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-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">
<p fxLayoutAlign="start center" class="pb-1">Rebalancing for Channel: {{selChannel.chan_id}}</p>
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<mat-form-field fxFlex="30">
<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}}">
<mat-hint>(Local Bal: {{selChannel?.local_balance}}, Remaining: {{selChannel?.local_balance - ((rebalanceAmount) ? rebalanceAmount : 0)}})</mat-hint>
<span matSuffix>Sats</span>
<mat-error *ngIf="!rebalanceAmount">Amount is required.</mat-error>
<mat-error *ngIf="amount.errors?.max">Amount must be less than or equal to {{selChannel?.local_balance}}.</mat-error>
</mat-form-field>
<mat-form-field fxFlex="30" fxLayoutAlign="start end">
<mat-select tabindex="2" [(value)]="selRebalancePeer" placeholder="Receive from Peer">
<mat-option *ngFor="let activeChannel of activeChannels" [value]="activeChannel">
{{activeChannel.remote_alias || activeChannel.chan_id}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="15" fxLayoutAlign="start end">
<mat-select tabindex="3" [(value)]="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="20">
<input matInput [(ngModel)]="feeLimit" [placeholder]="selFeeLimitType?.placeholder" type="number" name="feeLimit" step="1" min="0" required tabindex="4" #feeLmt="ngModel">
<mat-error *ngIf="!feeLimit">{{selFeeLimitType?.placeholder}} is required.</mat-error>
</mat-form-field>
</div>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="end center" fxFlex="100">
<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>
<button fxFlex="48" fxFlex.gt-sm="20" fxLayoutAlign="center center" mat-flat-button color="primary" (click)="onRebalance()" tabindex="6">Rebalance</button>
</div>
</form>
<div fxLayout="column" class="bordered-box p-2">
<p fxLayoutAlign="start center" class="pb-1">Rebalancing for Channel: {{selChannel.chan_id}}</p>
<mat-vertical-stepper [linear]="true" #stepper>
<mat-step [stepControl]="inputFormGroup">
<form [formGroup]="inputFormGroup" fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
<ng-template matStepLabel>Enter info to rebalance</ng-template>
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<mat-form-field fxFlex="48">
<input autoFocus matInput (change)="filterActiveChannels()" placeholder="Amount" type="number" step="100" tabindex="1" formControlName="rebalanceAmount" required>
<mat-hint>(Local Bal: {{selChannel?.local_balance}}, Remaining: {{selChannel?.local_balance - ((inputFormGroup.controls.rebalanceAmount.value) ? inputFormGroup.controls.rebalanceAmount.value : 0)}})</mat-hint>
<span matSuffix>Sats</span>
<mat-error *ngIf="inputFormGroup.controls.rebalanceAmount.errors?.required">Amount is required.</mat-error>
<mat-error *ngIf="inputFormGroup.controls.rebalanceAmount.errors?.min">Amount must be a positive number.</mat-error>
<mat-error *ngIf="inputFormGroup.controls.rebalanceAmount.errors?.max">Amount must be less than or equal to {{selChannel?.local_balance}}.</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48" fxLayoutAlign="start end">
<mat-select tabindex="2" formControlName="selRebalancePeer" placeholder="Receive from Peer" required>
<mat-option *ngFor="let activeChannel of activeChannels" [value]="activeChannel">
{{activeChannel.remote_alias || activeChannel.chan_id}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button mat-flat-button color="primary" tabindex="3" type="button" (click)="onEstimateFee()" matStepperNext>Estimate Fee</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="feeFormGroup">
<form [formGroup]="feeFormGroup" fxLayout="column" fxLayout.gt-sm="row wrap" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
<ng-template matStepLabel>Select rebalance fee</ng-template>
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<div fxFlex="75" class="alert">
<fa-icon [icon]="faInfoCircle" class="mr-1 alert-icon"></fa-icon>
<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>
</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 { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';
import { Actions } from '@ngrx/effects';
import { Actions, act } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { ChannelInformation } from '../../../models/alertData';
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 { LNDEffects } from '../../../../lnd/store/lnd.effects';
import * as fromRTLReducer from '../../../../store/rtl.reducers';
import * as RTLActions from '../../../../store/rtl.actions';
@ -19,58 +22,91 @@ import * as RTLActions from '../../../../store/rtl.actions';
styleUrls: ['./channel-rebalance.component.scss']
})
export class ChannelRebalanceComponent implements OnInit, OnDestroy {
@ViewChild('form', { static: false }) form: any;
public faInfoCircle = faInfoCircle;
public selChannel: Channel = {};
public rebalanceAmount = null;
public selRebalancePeer: Channel = {};
public activeChannels = [];
public feeLimit = null;
public selFeeLimitType = {id:'', name: '', placeholder: ''};
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()];
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() {
this.selChannel = this.data.channel;
FEE_LIMIT_TYPES.forEach((FEE_LIMIT_TYPE, i) => {
if(i > 0) {
this.feeLimitTypes.push(FEE_LIMIT_TYPE);
}
});
this.selFeeLimitType = this.feeLimitTypes[0];
this.selChannel = this.data.channel;
this.inputFormGroup = this.formBuilder.group({
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')
.pipe(takeUntil(this.unSubs[0]))
.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.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() {
if (!this.rebalanceAmount || this.rebalanceAmount <= 0 || this.rebalanceAmount > this.selChannel.local_balance || !this.feeLimit || !this.selRebalancePeer) { return true; }
this.store.dispatch(new RTLActions.OpenSpinner('Creating Invoice to Rebalance...'));
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.paymentRequest = '';
this.paymentStatus = null;
this.flgInvoiceGenerated = false;
this.flgPaymentSent = false;
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() {
this.activeChannels = this.activeChannels.filter(channel => channel.remote_balance >= this.rebalanceAmount && channel.chan_id !== this.selChannel.chan_id);
}
resetData() {
this.form.resetForm();
this.selFeeLimitType = this.feeLimitTypes[0];
this.selRebalancePeer = null;
this.activeChannels = this.activeChannels.filter(channel => channel.remote_balance >= this.inputFormGroup.controls.rebalanceAmount.value && channel.chan_id !== this.selChannel.chan_id);
}
onClose() {

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

@ -272,6 +272,10 @@ body {
margin-bottom: 2px !important;
}
.mb-5px {
margin-bottom: 5px !important;
}
.mb-1 {
margin-bottom: 1rem !important;
}
@ -759,7 +763,11 @@ body {
}
.h-6 {
height: 6rem;
height: 6rem !important;
}
.h-35px {
height: 3.5rem !important;
}
a {
@ -973,3 +981,7 @@ table {
.border-invalid {
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 DECODE_PAYMENT = 'DECODE_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 FETCH_GRAPH_NODE = 'FETCH_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 }) {}
}
export class SendPaymentStatus implements Action {
readonly type = SEND_PAYMENT_STATUS;
constructor(public payload: any) {}
}
export class FetchGraphNode implements Action {
readonly type = FETCH_GRAPH_NODE;
constructor(public payload: string) {} // payload = pubkey
@ -582,7 +588,7 @@ export class SetForwardingHistory implements Action {
export class GetQueryRoutes implements Action {
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 {
@ -851,7 +857,7 @@ export type RTLActions =
RestoreChannels | RestoreChannelsRes | RestoreChannelsList | SetRestoreChannelsList |
FetchTransactions | SetTransactions |
FetchInvoices | SetInvoices | SetTotalInvoices |
FetchPayments | SetPayments | SendPayment |
FetchPayments | SetPayments | SendPayment | SendPaymentStatus |
DecodePayment | SetDecodedPayment |
FetchGraphNode | SetGraphNode | GetQueryRoutes | SetQueryRoutes |
GetNewAddress | SetNewAddress | SetChannelTransaction |

Loading…
Cancel
Save