Add Fee Rate Information on Send Funds Modal

Closes #1410
This commit is contained in:
ShahanaFarooqui 2024-11-06 20:31:50 -08:00
parent e289c65d5a
commit 47baebb4f3
19 changed files with 117 additions and 50 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(()=>{"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],s=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(s=!1,f<a&&(a=f));if(s){e.splice(n--,1);var d=i();void 0!==d&&(o=d)}}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+"."+{17:"6fa7154eb6e447e2",190:"8009411a512731e5",193:"0e1a81316bbc29da",853:"50b06a24091d386f"}[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,s;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==o+f){a=u;break}}a||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",o+f),a.src=r.tu(t)),e[t]=[i];var c=(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(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{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={121: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(121!=i){var a=new Promise((u,c)=>n=e[i]=[u,c]);f.push(n[2]=a);var s=r.p+r.u(i),l=new Error;r.l(s,u=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var c=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+i+" failed.\n("+c+": "+p+")",l.name="ChunkLoadError",l.type=c,l.request=p,n[1](l)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var o=(i,f)=>{var l,d,[n,a,s]=f,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(s)var c=s(r)}for(i&&i(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[d]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))})()})();

View File

@ -1 +0,0 @@
(()=>{"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],s=!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):(s=!1,o<a&&(a=o));if(s){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+"."+{17:"11da55d0304111d4",190:"1b335a94fd7329bc",193:"0e1a81316bbc29da",853:"02f6281b873a27ae"}[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,s;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||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",f+o),a.src=r.tu(t)),e[t]=[i];var c=(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(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{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={121: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(121!=i){var a=new Promise((l,c)=>n=e[i]=[l,c]);o.push(n[2]=a);var s=r.p+r.u(i),d=new Error;r.l(s,l=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var c=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;d.message="Loading chunk "+i+" failed.\n("+c+": "+p+")",d.name="ChunkLoadError",d.type=c,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,s]=o,l=0;if(n.some(p=>0!==e[p])){for(d in a)r.o(a,d)&&(r.m[d]=a[d]);if(s)var c=s(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(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(f.bind(null,0)),t.push=f.bind(null,t.push.bind(t))})()})();

View File

@ -6,7 +6,20 @@
</div> </div>
<button tabindex="8" fxFlex="5" fxLayoutAlign="center center" class="btn-close-x p-0" default mat-button [mat-dialog-close]="false">X</button> <button tabindex="8" fxFlex="5" fxLayoutAlign="center center" class="btn-close-x p-0" default mat-button [mat-dialog-close]="false">X</button>
</mat-card-header> </mat-card-header>
<mat-card-content class="padding-gap-x-large"> <mat-card-content class="padding-gap-x-large" fxLayout="column">
<div *ngIf="recommendedFee.minimumFee" fxFlex="100" class="alert alert-info mb-2">
<fa-icon class="mr-1 alert-icon" [icon]="faInfoCircle" />
<span fxLayout="column" fxFlex="100">
<div>Fee rates recommended by mempool (sat/vByte):</div>
<span class="pr-2" fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between start">
<span>- High: {{recommendedFee.fastestFee || 'Unknown'}}</span>
<span>- Medium: {{recommendedFee.halfHourFee || 'Unknown'}}</span>
<span>- Low: {{recommendedFee.hourFee || 'Unknown'}}</span>
<span>- Economy: {{recommendedFee.economyFee || 'Unknown'}}</span>
<span>- Minimum: {{recommendedFee.minimumFee || 'Unknown'}}</span>
</span>
</span>
</div>
<form *ngIf="!sweepAll; else sweepAllBlock;" #form="ngForm" fxLayout="row wrap"fxLayoutAlign="space-between start" fxFlex="100" class="overflow-x-hidden" (submit)="onSendFunds()" (reset)="resetData()"> <form *ngIf="!sweepAll; else sweepAllBlock;" #form="ngForm" fxLayout="row wrap"fxLayoutAlign="space-between start" fxFlex="100" class="overflow-x-hidden" (submit)="onSendFunds()" (reset)="resetData()">
<mat-form-field fxLayout="column" fxFlex="55"> <mat-form-field fxLayout="column" fxFlex="55">
<mat-label>Bitcoin Address</mat-label> <mat-label>Bitcoin Address</mat-label>

View File

@ -8,7 +8,7 @@ import { Actions } from '@ngrx/effects';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatStepper } from '@angular/material/stepper'; import { MatStepper } from '@angular/material/stepper';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import * as sha256 from 'sha256'; import * as sha256 from 'sha256';
import { Node, RTLConfiguration } from '../../../shared/models/RTLconfig'; import { Node, RTLConfiguration } from '../../../shared/models/RTLconfig';
@ -25,6 +25,8 @@ import { setChannelTransaction } from '../../store/cln.actions';
import { rootAppConfig, rootSelectedNode } from '../../../store/rtl.selector'; import { rootAppConfig, rootSelectedNode } from '../../../store/rtl.selector';
import { clnNodeInformation, utxoBalances } from '../../store/cln.selector'; import { clnNodeInformation, utxoBalances } from '../../store/cln.selector';
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload'; import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
import { DataService } from 'src/app/shared/services/data.service';
import { RecommendedFeeRates } from 'src/app/shared/models/rtlModels';
@Component({ @Component({
selector: 'rtl-cln-on-chain-send-modal', selector: 'rtl-cln-on-chain-send-modal',
@ -37,6 +39,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
@ViewChild('formSweepAll', { static: false }) formSweepAll: any; @ViewChild('formSweepAll', { static: false }) formSweepAll: any;
@ViewChild('stepper', { static: false }) stepper: MatStepper; @ViewChild('stepper', { static: false }) stepper: MatStepper;
public faExclamationTriangle = faExclamationTriangle; public faExclamationTriangle = faExclamationTriangle;
public faInfoCircle = faInfoCircle;
public sweepAll = false; public sweepAll = false;
public selNode: Node | null; public selNode: Node | null;
public appConfig: RTLConfiguration; public appConfig: RTLConfiguration;
@ -65,6 +68,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
public advancedTitle = 'Advanced Options'; public advancedTitle = 'Advanced Options';
public flgValidated = false; public flgValidated = false;
public flgEditable = true; public flgEditable = true;
public recommendedFee: RecommendedFeeRates = { fastestFee: 0, halfHourFee: 0, hourFee: 0 };
public passwordFormLabel = 'Authenticate with your RTL password'; public passwordFormLabel = 'Authenticate with your RTL password';
public sendFundFormLabel = 'Sweep funds'; public sendFundFormLabel = 'Sweep funds';
public confirmFormLabel = 'Confirm sweep'; public confirmFormLabel = 'Confirm sweep';
@ -74,12 +78,13 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
confirmFormGroup: UntypedFormGroup; confirmFormGroup: UntypedFormGroup;
public screenSize = ''; public screenSize = '';
public screenSizeEnum = ScreenSizeEnum; public screenSizeEnum = ScreenSizeEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor( constructor(
public dialogRef: MatDialogRef<CLNOnChainSendModalComponent>, public dialogRef: MatDialogRef<CLNOnChainSendModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: CLNOnChainSendFunds, @Inject(MAT_DIALOG_DATA) public data: CLNOnChainSendFunds,
private logger: LoggerService, private logger: LoggerService,
private dataService: DataService,
private store: Store<RTLState>, private store: Store<RTLState>,
private commonService: CommonService, private commonService: CommonService,
private decimalPipe: DecimalPipe, private decimalPipe: DecimalPipe,
@ -91,6 +96,13 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.dataService.getRecommendedFeeRates().pipe(takeUntil(this.unSubs[0])).subscribe({
next: (rfRes: RecommendedFeeRates) => {
this.recommendedFee = rfRes;
}, error: (err) => {
this.logger.error(err);
}
});
this.sweepAll = this.data.sweepAll; this.sweepAll = this.data.sweepAll;
this.passwordFormGroup = this.formBuilder.group({ this.passwordFormGroup = this.formBuilder.group({
hiddenPassword: ['', [Validators.required]], hiddenPassword: ['', [Validators.required]],
@ -104,7 +116,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
minConfValue: [{ value: null, disabled: true }] minConfValue: [{ value: null, disabled: true }]
}); });
this.confirmFormGroup = this.formBuilder.group({}); this.confirmFormGroup = this.formBuilder.group({});
this.sendFundFormGroup.controls.flgMinConf.valueChanges.pipe(takeUntil(this.unSubs[0])).subscribe((flg) => { this.sendFundFormGroup.controls.flgMinConf.valueChanges.pipe(takeUntil(this.unSubs[1])).subscribe((flg) => {
if (flg) { if (flg) {
this.sendFundFormGroup.controls.selFeeRate.disable(); this.sendFundFormGroup.controls.selFeeRate.disable();
this.sendFundFormGroup.controls.selFeeRate.setValue(null); this.sendFundFormGroup.controls.selFeeRate.setValue(null);
@ -121,7 +133,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
this.sendFundFormGroup.controls.minConfValue.setErrors(null); this.sendFundFormGroup.controls.minConfValue.setErrors(null);
} }
}); });
this.sendFundFormGroup.controls.selFeeRate.valueChanges.pipe(takeUntil(this.unSubs[1])).subscribe((feeRate) => { this.sendFundFormGroup.controls.selFeeRate.valueChanges.pipe(takeUntil(this.unSubs[2])).subscribe((feeRate) => {
this.sendFundFormGroup.controls.customFeeRate.setValue(null); this.sendFundFormGroup.controls.customFeeRate.setValue(null);
this.sendFundFormGroup.controls.customFeeRate.reset(); this.sendFundFormGroup.controls.customFeeRate.reset();
if (feeRate === 'customperkb' && !this.sendFundFormGroup.controls.flgMinConf.value) { if (feeRate === 'customperkb' && !this.sendFundFormGroup.controls.flgMinConf.value) {
@ -130,23 +142,23 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
this.sendFundFormGroup.controls.customFeeRate.setValidators(null); this.sendFundFormGroup.controls.customFeeRate.setValidators(null);
} }
}); });
combineLatest([this.store.select(rootSelectedNode), this.store.select(rootAppConfig)]).pipe(takeUntil(this.unSubs[1])). combineLatest([this.store.select(rootSelectedNode), this.store.select(rootAppConfig)]).pipe(takeUntil(this.unSubs[3])).
subscribe(([selNode, appConfig]) => { subscribe(([selNode, appConfig]) => {
this.fiatConversion = selNode.settings.fiatConversion; this.fiatConversion = selNode.settings.fiatConversion;
this.amountUnits = selNode.settings.currencyUnits; this.amountUnits = selNode.settings.currencyUnits;
this.appConfig = appConfig; this.appConfig = appConfig;
}); });
this.store.select(clnNodeInformation).pipe(takeUntil(this.unSubs[2])). this.store.select(clnNodeInformation).pipe(takeUntil(this.unSubs[4])).
subscribe((nodeInfo: GetInfo) => { subscribe((nodeInfo: GetInfo) => {
this.information = nodeInfo; this.information = nodeInfo;
}); });
this.store.select(utxoBalances).pipe(takeUntil(this.unSubs[3])). this.store.select(utxoBalances).pipe(takeUntil(this.unSubs[5])).
subscribe((utxoBalancesSeletor: { utxos: UTXO[], balance: Balance, localRemoteBalance: LocalRemoteBalance, apiCallStatus: ApiCallStatusPayload }) => { subscribe((utxoBalancesSeletor: { utxos: UTXO[], balance: Balance, localRemoteBalance: LocalRemoteBalance, apiCallStatus: ApiCallStatusPayload }) => {
this.utxos = this.commonService.sortAscByKey(utxoBalancesSeletor.utxos?.filter((utxo) => utxo.status === 'confirmed'), 'value'); this.utxos = this.commonService.sortAscByKey(utxoBalancesSeletor.utxos?.filter((utxo) => utxo.status === 'confirmed'), 'value');
this.logger.info(utxoBalancesSeletor); this.logger.info(utxoBalancesSeletor);
}); });
this.actions.pipe( this.actions.pipe(
takeUntil(this.unSubs[4]), takeUntil(this.unSubs[6]),
filter((action) => action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN || action.type === CLNActions.SET_CHANNEL_TRANSACTION_RES_CLN)). filter((action) => action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN || action.type === CLNActions.SET_CHANNEL_TRANSACTION_RES_CLN)).
subscribe((action: any) => { subscribe((action: any) => {
if (action.type === CLNActions.SET_CHANNEL_TRANSACTION_RES_CLN) { if (action.type === CLNActions.SET_CHANNEL_TRANSACTION_RES_CLN) {
@ -219,7 +231,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
} }
if (this.transaction.satoshi && this.transaction.satoshi !== 'all' && this.selAmountUnit !== CurrencyUnitEnum.SATS) { if (this.transaction.satoshi && this.transaction.satoshi !== 'all' && this.selAmountUnit !== CurrencyUnitEnum.SATS) {
this.commonService.convertCurrency(+this.transaction.satoshi, this.selAmountUnit === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : this.selAmountUnit, CurrencyUnitEnum.SATS, this.amountUnits[2], this.fiatConversion). this.commonService.convertCurrency(+this.transaction.satoshi, this.selAmountUnit === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : this.selAmountUnit, CurrencyUnitEnum.SATS, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[5])). pipe(takeUntil(this.unSubs[7])).
subscribe({ subscribe({
next: (data) => { next: (data) => {
this.transaction.satoshi = data[CurrencyUnitEnum.SATS]; this.transaction.satoshi = data[CurrencyUnitEnum.SATS];
@ -307,7 +319,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
let currSelectedUnit = event.value === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : event.value; let currSelectedUnit = event.value === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : event.value;
if (this.transaction.satoshi && this.selAmountUnit !== event.value) { if (this.transaction.satoshi && this.selAmountUnit !== event.value) {
this.commonService.convertCurrency(+this.transaction.satoshi, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion). this.commonService.convertCurrency(+this.transaction.satoshi, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[6])). pipe(takeUntil(this.unSubs[8])).
subscribe({ subscribe({
next: (data) => { next: (data) => {
this.selAmountUnit = event.value; this.selAmountUnit = event.value;

View File

@ -58,7 +58,7 @@
<div fxFlex="64" fxLayout="row" fxLayoutAlign="space-between center"> <div fxFlex="64" fxLayout="row" fxLayoutAlign="space-between center">
<mat-form-field fxLayout="column" fxLayoutAlign="start center" [fxFlex]="selFeeRate === 'customperkb' && !flgMinConf ? '40' : '100'"> <mat-form-field fxLayout="column" fxLayoutAlign="start center" [fxFlex]="selFeeRate === 'customperkb' && !flgMinConf ? '40' : '100'">
<mat-label>Fee Rate</mat-label> <mat-label>Fee Rate</mat-label>
<mat-select tabindex="4" [disabled]="flgMinConf" [(value)]="selFeeRate" (selectionChange)="onSelFeeRateChanged($event)"> <mat-select tabindex="4" [disabled]="flgMinConf" [(value)]="selFeeRate">
<mat-option *ngFor="let feeRateType of feeRateTypes" [value]="feeRateType.feeRateId"> <mat-option *ngFor="let feeRateType of feeRateTypes" [value]="feeRateType.feeRateId">
{{feeRateType.feeRateType}} {{feeRateType.feeRateType}}
</mat-option> </mat-option>

View File

@ -168,6 +168,13 @@ export class CLNOpenChannelComponent implements OnInit, OnDestroy {
} }
} else { } else {
this.advancedTitle = 'Advanced Options'; this.advancedTitle = 'Advanced Options';
this.dataService.getRecommendedFeeRates().pipe(takeUntil(this.unSubs[3])).subscribe({
next: (rfRes: RecommendedFeeRates) => {
this.recommendedFee = rfRes;
}, error: (err) => {
this.logger.error(err);
}
});
} }
} }
@ -209,19 +216,6 @@ export class CLNOpenChannelComponent implements OnInit, OnDestroy {
this.store.dispatch(saveNewChannel({ payload: newChannel })); this.store.dispatch(saveNewChannel({ payload: newChannel }));
} }
onSelFeeRateChanged(event) {
this.customFeeRate = null;
if (event.value === 'customperkb') {
this.dataService.getRecommendedFeeRates().pipe(takeUntil(this.unSubs[3])).subscribe({
next: (rfRes: RecommendedFeeRates) => {
this.recommendedFee = rfRes;
}, error: (err) => {
this.logger.error(err);
}
});
}
}
ngOnDestroy() { ngOnDestroy() {
this.unSubs.forEach((completeSub) => { this.unSubs.forEach((completeSub) => {
completeSub.next(<any>null); completeSub.next(<any>null);

View File

@ -608,10 +608,9 @@ export class CLNEffects implements OnDestroy {
ofType(CLNActions.DELETE_EXPIRED_INVOICE_CLN), ofType(CLNActions.DELETE_EXPIRED_INVOICE_CLN),
mergeMap((action: { type: string, payload: number }) => { mergeMap((action: { type: string, payload: number }) => {
this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DELETE_INVOICE })); this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DELETE_INVOICE }));
return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.INVOICES_API + '/delete', { 'subsystem': 'expiredinvoices', 'age': SECS_IN_YEAR }). return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.INVOICES_API + '/delete', { subsystem: 'expiredinvoices', age: SECS_IN_YEAR }).
pipe( pipe(
map((postRes: any) => { map((postRes: any) => {
console.warn(postRes);
this.logger.info(postRes); this.logger.info(postRes);
this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DELETE_INVOICE })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DELETE_INVOICE }));
this.store.dispatch(openSnackBar({ payload: postRes.status })); this.store.dispatch(openSnackBar({ payload: postRes.status }));

View File

@ -6,7 +6,20 @@
</div> </div>
<button tabindex="8" fxFlex="5" fxLayoutAlign="center center" class="btn-close-x p-0" default mat-button [mat-dialog-close]="false">X</button> <button tabindex="8" fxFlex="5" fxLayoutAlign="center center" class="btn-close-x p-0" default mat-button [mat-dialog-close]="false">X</button>
</mat-card-header> </mat-card-header>
<mat-card-content class="padding-gap-x-large"> <mat-card-content class="padding-gap-x-large" fxLayout="column">
<div *ngIf="recommendedFee.minimumFee" fxFlex="100" class="alert alert-info mb-2">
<fa-icon class="mr-1 alert-icon" [icon]="faInfoCircle" />
<span fxLayout="column" fxFlex="100">
<div>Fee rates recommended by mempool (sat/vByte):</div>
<span class="pr-2" fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between start">
<span>- High: {{recommendedFee.fastestFee || 'Unknown'}}</span>
<span>- Medium: {{recommendedFee.halfHourFee || 'Unknown'}}</span>
<span>- Low: {{recommendedFee.hourFee || 'Unknown'}}</span>
<span>- Economy: {{recommendedFee.economyFee || 'Unknown'}}</span>
<span>- Minimum: {{recommendedFee.minimumFee || 'Unknown'}}</span>
</span>
</span>
</div>
<form #form="ngForm" fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between start" class="overflow-x-hidden" (submit)="onSendFunds()" (reset)="resetData()"> <form #form="ngForm" fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between start" class="overflow-x-hidden" (submit)="onSendFunds()" (reset)="resetData()">
<mat-form-field fxLayout="column" fxFlex="55"> <mat-form-field fxLayout="column" fxFlex="55">
<mat-label>Bitcoin Address</mat-label> <mat-label>Bitcoin Address</mat-label>

View File

@ -5,7 +5,7 @@ import { takeUntil, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects'; import { Actions } from '@ngrx/effects';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { Node } from '../../../shared/models/RTLconfig'; import { Node } from '../../../shared/models/RTLconfig';
import { GetInfo, OnChainBalance, SendPaymentOnChain } from '../../../shared/models/eclModels'; import { GetInfo, OnChainBalance, SendPaymentOnChain } from '../../../shared/models/eclModels';
@ -17,6 +17,8 @@ import { RTLState } from '../../../store/rtl.state';
import { openSnackBar } from '../../../store/rtl.actions'; import { openSnackBar } from '../../../store/rtl.actions';
import { sendOnchainFunds } from '../../store/ecl.actions'; import { sendOnchainFunds } from '../../store/ecl.actions';
import { rootSelectedNode } from '../../../store/rtl.selector'; import { rootSelectedNode } from '../../../store/rtl.selector';
import { DataService } from 'src/app/shared/services/data.service';
import { RecommendedFeeRates } from 'src/app/shared/models/rtlModels';
@Component({ @Component({
selector: 'rtl-ecl-on-chain-send-modal', selector: 'rtl-ecl-on-chain-send-modal',
@ -27,6 +29,7 @@ export class ECLOnChainSendModalComponent implements OnInit, OnDestroy {
@ViewChild('form', { static: true }) form: any; @ViewChild('form', { static: true }) form: any;
public faExclamationTriangle = faExclamationTriangle; public faExclamationTriangle = faExclamationTriangle;
public faInfoCircle = faInfoCircle;
public selNode: Node | null; public selNode: Node | null;
public addressTypes = []; public addressTypes = [];
public selectedAddress = ADDRESS_TYPES[1]; public selectedAddress = ADDRESS_TYPES[1];
@ -41,19 +44,27 @@ export class ECLOnChainSendModalComponent implements OnInit, OnDestroy {
public currConvertorRate = {}; public currConvertorRate = {};
public unitConversionValue = 0; public unitConversionValue = 0;
public currencyUnitFormats = CURRENCY_UNIT_FORMATS; public currencyUnitFormats = CURRENCY_UNIT_FORMATS;
public recommendedFee: RecommendedFeeRates = { fastestFee: 0, halfHourFee: 0, hourFee: 0 };
public amountError = 'Amount is Required.'; public amountError = 'Amount is Required.';
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<ECLOnChainSendModalComponent>, private logger: LoggerService, private store: Store<RTLState>, private commonService: CommonService, private decimalPipe: DecimalPipe, private actions: Actions) { } constructor(public dialogRef: MatDialogRef<ECLOnChainSendModalComponent>, private logger: LoggerService, private dataService: DataService, private store: Store<RTLState>, private commonService: CommonService, private decimalPipe: DecimalPipe, private actions: Actions) { }
ngOnInit() { ngOnInit() {
this.store.select(rootSelectedNode).pipe(takeUntil(this.unSubs[0])).subscribe((selNode) => { this.dataService.getRecommendedFeeRates().pipe(takeUntil(this.unSubs[0])).subscribe({
next: (rfRes: RecommendedFeeRates) => {
this.recommendedFee = rfRes;
}, error: (err) => {
this.logger.error(err);
}
});
this.store.select(rootSelectedNode).pipe(takeUntil(this.unSubs[1])).subscribe((selNode) => {
this.fiatConversion = selNode.settings.fiatConversion; this.fiatConversion = selNode.settings.fiatConversion;
this.amountUnits = selNode.settings.currencyUnits; this.amountUnits = selNode.settings.currencyUnits;
this.logger.info(selNode); this.logger.info(selNode);
}); });
this.actions.pipe( this.actions.pipe(
takeUntil(this.unSubs[1]), takeUntil(this.unSubs[2]),
filter((action) => action.type === ECLActions.UPDATE_API_CALL_STATUS_ECL || action.type === ECLActions.SEND_ONCHAIN_FUNDS_RES_ECL) filter((action) => action.type === ECLActions.UPDATE_API_CALL_STATUS_ECL || action.type === ECLActions.SEND_ONCHAIN_FUNDS_RES_ECL)
). ).
subscribe((action: any) => { subscribe((action: any) => {
@ -74,7 +85,7 @@ export class ECLOnChainSendModalComponent implements OnInit, OnDestroy {
this.sendFundError = ''; this.sendFundError = '';
if (this.transaction.amount && this.selAmountUnit !== CurrencyUnitEnum.SATS) { if (this.transaction.amount && this.selAmountUnit !== CurrencyUnitEnum.SATS) {
this.commonService.convertCurrency(this.transaction.amount, this.selAmountUnit === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : this.selAmountUnit, CurrencyUnitEnum.SATS, this.amountUnits[2], this.fiatConversion). this.commonService.convertCurrency(this.transaction.amount, this.selAmountUnit === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : this.selAmountUnit, CurrencyUnitEnum.SATS, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[2])). pipe(takeUntil(this.unSubs[3])).
subscribe({ subscribe({
next: (data) => { next: (data) => {
this.transaction.amount = parseInt(data[CurrencyUnitEnum.SATS]); this.transaction.amount = parseInt(data[CurrencyUnitEnum.SATS]);
@ -107,7 +118,7 @@ export class ECLOnChainSendModalComponent implements OnInit, OnDestroy {
let currSelectedUnit = event.value === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : event.value; let currSelectedUnit = event.value === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : event.value;
if (this.transaction.amount && this.selAmountUnit !== event.value) { if (this.transaction.amount && this.selAmountUnit !== event.value) {
this.commonService.convertCurrency(this.transaction.amount, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion). this.commonService.convertCurrency(this.transaction.amount, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[3])). pipe(takeUntil(this.unSubs[4])).
subscribe({ subscribe({
next: (data) => { next: (data) => {
this.selAmountUnit = event.value; this.selAmountUnit = event.value;

View File

@ -6,8 +6,21 @@
</div> </div>
<button tabindex="8" fxFlex="5" fxLayoutAlign="center center" class="btn-close-x p-0" default mat-button [mat-dialog-close]="false">X</button> <button tabindex="8" fxFlex="5" fxLayoutAlign="center center" class="btn-close-x p-0" default mat-button [mat-dialog-close]="false">X</button>
</mat-card-header> </mat-card-header>
<mat-card-content class="padding-gap-x-large"> <mat-card-content class="padding-gap-x-large" fxLayout="column">
<form *ngIf="!sweepAll; else sweepAllBlock;" #form="ngForm" fxLayout="row wrap"fxLayoutAlign="space-between start" fxFlex="100" class="overflow-x-hidden" (submit)="onSendFunds()" (reset)="resetData()"> <div *ngIf="recommendedFee.minimumFee" fxFlex="100" class="alert alert-info mb-2">
<fa-icon class="mr-1 alert-icon" [icon]="faInfoCircle" />
<span fxLayout="column" fxFlex="100">
<div>Fee rates recommended by mempool (sat/vByte):</div>
<span class="pr-2" fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between start">
<span>- High: {{recommendedFee.fastestFee || 'Unknown'}}</span>
<span>- Medium: {{recommendedFee.halfHourFee || 'Unknown'}}</span>
<span>- Low: {{recommendedFee.hourFee || 'Unknown'}}</span>
<span>- Economy: {{recommendedFee.economyFee || 'Unknown'}}</span>
<span>- Minimum: {{recommendedFee.minimumFee || 'Unknown'}}</span>
</span>
</span>
</div>
<form *ngIf="!sweepAll; else sweepAllBlock;" #form="ngForm" fxLayout="row wrap"fxLayoutAlign="space-between start" fxFlex="100" class="overflow-x-hidden" (submit)="onSendFunds()" (reset)="resetData()">
<mat-form-field fxLayout="column" fxFlex.gt-sm="55"> <mat-form-field fxLayout="column" fxFlex.gt-sm="55">
<mat-label>Bitcoin Address</mat-label> <mat-label>Bitcoin Address</mat-label>
<input #address="ngModel" autoFocus matInput tabindex="1" name="address" required [(ngModel)]="transactionAddress"> <input #address="ngModel" autoFocus matInput tabindex="1" name="address" required [(ngModel)]="transactionAddress">

View File

@ -8,7 +8,7 @@ import { Actions } from '@ngrx/effects';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatStepper } from '@angular/material/stepper'; import { MatStepper } from '@angular/material/stepper';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { OnChainSendFunds } from '../../../shared/models/alertData'; import { OnChainSendFunds } from '../../../shared/models/alertData';
import { Node, RTLConfiguration } from '../../../shared/models/RTLconfig'; import { Node, RTLConfiguration } from '../../../shared/models/RTLconfig';
@ -23,6 +23,8 @@ import { RTLState } from '../../../store/rtl.state';
import { isAuthorized, openSnackBar } from '../../../store/rtl.actions'; import { isAuthorized, openSnackBar } from '../../../store/rtl.actions';
import { setChannelTransaction } from '../../store/lnd.actions'; import { setChannelTransaction } from '../../store/lnd.actions';
import { rootAppConfig, rootSelectedNode } from '../../../store/rtl.selector'; import { rootAppConfig, rootSelectedNode } from '../../../store/rtl.selector';
import { DataService } from 'src/app/shared/services/data.service';
import { RecommendedFeeRates } from 'src/app/shared/models/rtlModels';
@Component({ @Component({
selector: 'rtl-on-chain-send-modal', selector: 'rtl-on-chain-send-modal',
@ -35,6 +37,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
@ViewChild('formSweepAll', { static: false }) formSweepAll: any; @ViewChild('formSweepAll', { static: false }) formSweepAll: any;
@ViewChild('stepper', { static: false }) stepper: MatStepper; @ViewChild('stepper', { static: false }) stepper: MatStepper;
public faExclamationTriangle = faExclamationTriangle; public faExclamationTriangle = faExclamationTriangle;
public faInfoCircle = faInfoCircle;
public sweepAll = false; public sweepAll = false;
public selNode: Node | null; public selNode: Node | null;
public appConfig: RTLConfiguration; public appConfig: RTLConfiguration;
@ -58,6 +61,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
public sendFundError = ''; public sendFundError = '';
public flgValidated = false; public flgValidated = false;
public flgEditable = true; public flgEditable = true;
public recommendedFee: RecommendedFeeRates = { fastestFee: 0, halfHourFee: 0, hourFee: 0 };
public passwordFormLabel = 'Authenticate with your RTL password'; public passwordFormLabel = 'Authenticate with your RTL password';
public sendFundFormLabel = 'Sweep funds'; public sendFundFormLabel = 'Sweep funds';
public confirmFormLabel = 'Confirm sweep'; public confirmFormLabel = 'Confirm sweep';
@ -65,12 +69,13 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
passwordFormGroup: UntypedFormGroup; passwordFormGroup: UntypedFormGroup;
sendFundFormGroup: UntypedFormGroup; sendFundFormGroup: UntypedFormGroup;
confirmFormGroup: UntypedFormGroup; confirmFormGroup: UntypedFormGroup;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor( constructor(
public dialogRef: MatDialogRef<OnChainSendModalComponent>, public dialogRef: MatDialogRef<OnChainSendModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: OnChainSendFunds, @Inject(MAT_DIALOG_DATA) public data: OnChainSendFunds,
private logger: LoggerService, private logger: LoggerService,
private dataService: DataService,
private store: Store<RTLState>, private store: Store<RTLState>,
private rtlEffects: RTLEffects, private rtlEffects: RTLEffects,
private commonService: CommonService, private commonService: CommonService,
@ -80,6 +85,13 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
private formBuilder: UntypedFormBuilder) { } private formBuilder: UntypedFormBuilder) { }
ngOnInit() { ngOnInit() {
this.dataService.getRecommendedFeeRates().pipe(takeUntil(this.unSubs[0])).subscribe({
next: (rfRes: RecommendedFeeRates) => {
this.recommendedFee = rfRes;
}, error: (err) => {
this.logger.error(err);
}
});
this.sweepAll = this.data.sweepAll; this.sweepAll = this.data.sweepAll;
this.passwordFormGroup = this.formBuilder.group({ this.passwordFormGroup = this.formBuilder.group({
hiddenPassword: ['', [Validators.required]], hiddenPassword: ['', [Validators.required]],
@ -92,7 +104,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
selTransType: ['1', Validators.required] selTransType: ['1', Validators.required]
}); });
this.confirmFormGroup = this.formBuilder.group({}); this.confirmFormGroup = this.formBuilder.group({});
this.sendFundFormGroup.controls.selTransType.valueChanges.pipe(takeUntil(this.unSubs[0])).subscribe((transType) => { this.sendFundFormGroup.controls.selTransType.valueChanges.pipe(takeUntil(this.unSubs[1])).subscribe((transType) => {
if (transType === '1') { if (transType === '1') {
this.sendFundFormGroup.controls.transactionBlocks.setValidators([Validators.required]); this.sendFundFormGroup.controls.transactionBlocks.setValidators([Validators.required]);
this.sendFundFormGroup.controls.transactionBlocks.setValue(null); this.sendFundFormGroup.controls.transactionBlocks.setValue(null);
@ -105,16 +117,16 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
this.sendFundFormGroup.controls.transactionFees.setValue(null); this.sendFundFormGroup.controls.transactionFees.setValue(null);
} }
}); });
this.store.select(rootAppConfig).pipe(takeUntil(this.unSubs[1])).subscribe((appConfig) => { this.store.select(rootAppConfig).pipe(takeUntil(this.unSubs[2])).subscribe((appConfig) => {
this.appConfig = appConfig; this.appConfig = appConfig;
}); });
this.store.select(rootSelectedNode).pipe(takeUntil(this.unSubs[2])).subscribe((selNode) => { this.store.select(rootSelectedNode).pipe(takeUntil(this.unSubs[3])).subscribe((selNode) => {
this.fiatConversion = selNode.settings.fiatConversion; this.fiatConversion = selNode.settings.fiatConversion;
this.amountUnits = selNode.settings.currencyUnits; this.amountUnits = selNode.settings.currencyUnits;
this.logger.info(selNode); this.logger.info(selNode);
}); });
this.actions.pipe( this.actions.pipe(
takeUntil(this.unSubs[3]), takeUntil(this.unSubs[4]),
filter((action) => action.type === LNDActions.UPDATE_API_CALL_STATUS_LND || action.type === LNDActions.SET_CHANNEL_TRANSACTION_RES_LND)). filter((action) => action.type === LNDActions.UPDATE_API_CALL_STATUS_LND || action.type === LNDActions.SET_CHANNEL_TRANSACTION_RES_LND)).
subscribe((action: any) => { subscribe((action: any) => {
if (action.type === LNDActions.SET_CHANNEL_TRANSACTION_RES_LND) { if (action.type === LNDActions.SET_CHANNEL_TRANSACTION_RES_LND) {
@ -173,7 +185,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
} }
if (this.transactionAmount && this.selAmountUnit !== CurrencyUnitEnum.SATS) { if (this.transactionAmount && this.selAmountUnit !== CurrencyUnitEnum.SATS) {
this.commonService.convertCurrency(this.transactionAmount, this.selAmountUnit === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : this.selAmountUnit, CurrencyUnitEnum.SATS, this.amountUnits[2], this.fiatConversion). this.commonService.convertCurrency(this.transactionAmount, this.selAmountUnit === this.amountUnits[2] ? CurrencyUnitEnum.OTHER : this.selAmountUnit, CurrencyUnitEnum.SATS, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[4])). pipe(takeUntil(this.unSubs[5])).
subscribe({ subscribe({
next: (data) => { next: (data) => {
this.selAmountUnit = CurrencyUnitEnum.SATS; this.selAmountUnit = CurrencyUnitEnum.SATS;
@ -254,7 +266,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
if (this.transactionAmount && this.selAmountUnit !== event.value) { if (this.transactionAmount && this.selAmountUnit !== event.value) {
const amount = this.transactionAmount ? this.transactionAmount : 0; const amount = this.transactionAmount ? this.transactionAmount : 0;
this.commonService.convertCurrency(amount, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion). this.commonService.convertCurrency(amount, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[5])). pipe(takeUntil(this.unSubs[6])).
subscribe({ subscribe({
next: (data) => { next: (data) => {
this.selAmountUnit = event.value; this.selAmountUnit = event.value;