Bump fee with mempool information (#1386)

pull/1391/head
ShahanaFarooqui 3 weeks ago committed by GitHub
parent 45d1c43609
commit 40f6c4d933
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -14,6 +14,54 @@ const logger = Logger;
const common = Common;
const wsServer = WSServer;
const databaseService = Database;
// Set local block explorer URL after first API call
// if the selected node block explorer has working REST API suite
// otherwise set it to mempool.space
let blockExplorerUrl = '';
export const getExplorerFeesRecommended = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting Recommended Fee Rates..' });
options.url = (blockExplorerUrl === '') ?
req.session.selectedNode.settings.blockExplorerUrl + '/api/v1/fees/recommended' :
blockExplorerUrl + '/api/v1/fees/recommended';
request(options).then((body) => {
blockExplorerUrl = req.session.selectedNode.settings.blockExplorerUrl;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Recommended Fee Rates Received', data: body });
res.status(200).json(JSON.parse(body));
}).catch((errRes) => {
blockExplorerUrl = 'https://mempool.space';
options.url = blockExplorerUrl + '/api/v1/fees/recommended';
return request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Recommended Fee Rates Received', data: body });
res.status(200).json(JSON.parse(body));
}).catch((errRes) => {
const errMsg = 'Get Recommended Fee Rates Error';
const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.error, error: err.error });
});
});
};
export const getExplorerTransaction = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting Transaction From Block Explorer..' });
options.url = (blockExplorerUrl === '') ?
req.session.selectedNode.settings.blockExplorerUrl + '/api/tx/' + req.params.txid :
blockExplorerUrl + '/api/tx/' + req.params.txid;
request(options).then((body) => {
blockExplorerUrl = req.session.selectedNode.settings.blockExplorerUrl;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Transaction From Block Explorer Received', data: body });
res.status(200).json(JSON.parse(body));
}).catch((errRes) => {
blockExplorerUrl = 'https://mempool.space';
options.url = blockExplorerUrl + '/api/tx/' + req.params.txid;
return request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Transaction From Block Explorer Received', data: body });
res.status(200).json(JSON.parse(body));
}).catch((errRes) => {
const errMsg = 'Get Transaction From Block Explorer Error';
const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.error, error: err.error });
});
});
};
export const getCurrencyRates = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting Currency Rates..' });
options.url = 'https://blockchain.info/ticker';
@ -99,6 +147,7 @@ export const updateSelectedNode = (req, res, next) => {
databaseService.loadDatabase(req.session);
}
}
blockExplorerUrl = '';
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Selected Node Updated To ' + req.session.selectedNode.lnNode || '' });
res.status(200).json(common.removeAuthSecureData(JSON.parse(JSON.stringify(req.session.selectedNode))));
};

@ -1,7 +1,7 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { updateNodeSettings, getConfig, getFile, updateSelectedNode, updateApplicationSettings, getCurrencyRates, getApplicationSettings } from '../../controllers/shared/RTLConf.js';
import { updateNodeSettings, getConfig, getFile, updateSelectedNode, updateApplicationSettings, getCurrencyRates, getApplicationSettings, getExplorerFeesRecommended, getExplorerTransaction } from '../../controllers/shared/RTLConf.js';
const router = Router();
router.get('/', getApplicationSettings);
router.get('/rates', getCurrencyRates);
@ -10,4 +10,6 @@ router.get('/updateSelNode/:currNodeIndex/:prevNodeIndex', updateSelectedNode);
router.get('/config/:nodeType', isAuthenticated, getConfig);
router.post('/node', isAuthenticated, updateNodeSettings);
router.post('/application', isAuthenticated, updateApplicationSettings);
router.get('/explorerFeesRecommended', getExplorerFeesRecommended);
router.get('/explorerTransaction/:txid', getExplorerTransaction);
export default router;

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

@ -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],c=!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):(c=!1,f<a&&(a=f));if(c){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+"."+{125:"2d8b0d451f9e6528",456:"a7433b9c5b34e0df",570:"a47a5e74ba9177e8",758:"b6dcd2f2b36dacf0"}[e]+".js",r.miniCssF=e=>{},r.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),(()=>{var e={},o="RTLApp:";r.l=(t,i,f,n)=>{if(e[t])e[t].push(i);else{var a,c;if(void 0!==f)for(var 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||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",o+f),a.src=r.tu(t)),e[t]=[i];var s=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(y=>y(b)),g)return g(b)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:o=>o},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(i,f)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=i){var a=new Promise((u,s)=>n=e[i]=[u,s]);f.push(n[2]=a);var c=r.p+r.u(i),l=new Error;r.l(c,u=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+i+" failed.\n("+s+": "+p+")",l.name="ChunkLoadError",l.type=s,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,c]=f,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(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(s)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))})()})();

@ -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,l=0;l<t.length;l++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(s=!1,o<a&&(a=o));if(s){e.splice(n--,1);var d=i();void 0!==d&&(f=d)}}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+"."+{125:"05cbc1feb7174532",456:"a7433b9c5b34e0df",570:"a97682bb2ea30f3d",758:"b6dcd2f2b36dacf0"}[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 l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==f+o){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",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={666:0};r.f.j=(i,o)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)o.push(n[2]);else if(666!=i){var a=new Promise((u,c)=>n=e[i]=[u,c]);o.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 f=(i,o)=>{var l,d,[n,a,s]=o,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(o);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(f.bind(null,0)),t.push=f.bind(null,t.push.bind(t))})()})();

@ -15,6 +15,56 @@ const logger: LoggerService = Logger;
const common: CommonService = Common;
const wsServer = WSServer;
const databaseService: DatabaseService = Database;
// Set local block explorer URL after first API call
// if the selected node block explorer has working REST API suite
// otherwise set it to mempool.space
let blockExplorerUrl = '';
export const getExplorerFeesRecommended = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting Recommended Fee Rates..' });
options.url = (blockExplorerUrl === '') ?
req.session.selectedNode.settings.blockExplorerUrl + '/api/v1/fees/recommended' :
blockExplorerUrl + '/api/v1/fees/recommended';
request(options).then((body) => {
blockExplorerUrl = req.session.selectedNode.settings.blockExplorerUrl;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Recommended Fee Rates Received', data: body });
res.status(200).json(JSON.parse(body));
}).catch((errRes) => {
blockExplorerUrl = 'https://mempool.space';
options.url = blockExplorerUrl + '/api/v1/fees/recommended';
return request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Recommended Fee Rates Received', data: body });
res.status(200).json(JSON.parse(body));
}).catch((errRes) => {
const errMsg = 'Get Recommended Fee Rates Error';
const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.error, error: err.error });
});
});
};
export const getExplorerTransaction = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting Transaction From Block Explorer..' });
options.url = (blockExplorerUrl === '') ?
req.session.selectedNode.settings.blockExplorerUrl + '/api/tx/' + req.params.txid :
blockExplorerUrl + '/api/tx/' + req.params.txid;
request(options).then((body) => {
blockExplorerUrl = req.session.selectedNode.settings.blockExplorerUrl;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Transaction From Block Explorer Received', data: body });
res.status(200).json(JSON.parse(body));
}).catch((errRes) => {
blockExplorerUrl = 'https://mempool.space';
options.url = blockExplorerUrl + '/api/tx/' + req.params.txid;
return request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Transaction From Block Explorer Received', data: body });
res.status(200).json(JSON.parse(body));
}).catch((errRes) => {
const errMsg = 'Get Transaction From Block Explorer Error';
const err = common.handleError({ statusCode: 500, message: errMsg, error: errRes }, 'RTLConf', errMsg, req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.error, error: err.error });
});
});
};
export const getCurrencyRates = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Getting Currency Rates..' });
@ -100,6 +150,7 @@ export const updateSelectedNode = (req, res, next) => {
databaseService.loadDatabase(req.session);
}
}
blockExplorerUrl = '';
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'RTLConf', msg: 'Selected Node Updated To ' + req.session.selectedNode.lnNode || '' });
res.status(200).json(common.removeAuthSecureData(JSON.parse(JSON.stringify(req.session.selectedNode))));
};

@ -1,7 +1,8 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { updateNodeSettings, getConfig, getFile, updateSelectedNode, updateApplicationSettings, getCurrencyRates, getApplicationSettings } from '../../controllers/shared/RTLConf.js';
import { updateNodeSettings, getConfig, getFile, updateSelectedNode, updateApplicationSettings, getCurrencyRates, getApplicationSettings,
getExplorerFeesRecommended, getExplorerTransaction } from '../../controllers/shared/RTLConf.js';
const router = Router();
@ -12,5 +13,7 @@ router.get('/updateSelNode/:currNodeIndex/:prevNodeIndex', updateSelectedNode);
router.get('/config/:nodeType', isAuthenticated, getConfig);
router.post('/node', isAuthenticated, updateNodeSettings);
router.post('/application', isAuthenticated, updateApplicationSettings);
router.get('/explorerFeesRecommended', getExplorerFeesRecommended);
router.get('/explorerTransaction/:txid', getExplorerTransaction);
export default router;

@ -15,15 +15,17 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch">
<div fxFlex="100" class="alert alert-info">
<fa-icon class="mr-1 alert-icon" [icon]="faInfoCircle" />
<span fxLayout="column" fxFlex="100">Bumping fee on pending open channels is an advanced feature, attempt it only if you are familiar with the functionality of Bitcoin transactions.
<div>Before attempting fee bump ensure the following:</div>
<div class="pl-1">1: Use a Bitcoin block explorer to ensure that channel opening transaction is not confirmed.</div>
<div class="pl-1">2: The channel opening transaction must have a sizable change output, which can be spent further. The fee cannot be bumped without the change output.</div>
<div class="pl-1">3: Find the index value of the change output via a block explorer.</div>
<div class="pl-1">4: Enter the index value of the change output in the form below and the desired fee rate.</div>
<div class="pl-1">5: Upon successful fee bump, use your block explorer to track the child transaction in the mempool, which should be linked with the change output transaction.</div>
<span fxLayout="column" fxFlex="100">
<div>Fee rates recommended by mempool.space (sat/vByte):</div>
<div>- High: {{recommendedFee.fastestFee || 'Unknown'}}</div>
<div>- Medium: {{recommendedFee.halfHourFee || 'Unknown'}}</div>
<div>- Low: {{recommendedFee.hourFee || 'Unknown'}}</div>
</span>
</div>
<div *ngIf="flgShowDustWarning" fxFlex="100" class="alert alert-warn">
<fa-icon class="mr-1 alert-icon" [icon]="faExclamationTriangle" />
<span>Change output balance <strong>{{dustOutputValue | number}}</strong> (Sats) may be insufficient for fee bumping, depending on the prevailing fee rates.</span>
</div>
<div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between center">
<mat-form-field fxLayout="column" fxFlex="49">
<mat-label>Output Index</mat-label>

@ -8,10 +8,12 @@ import { faCopy, faInfoCircle, faExclamationTriangle } from '@fortawesome/free-s
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RecommendedFeeRates, BlockExplorerTransaction } from '../../../../shared/models/rtlModels';
import { Channel } from '../../../../shared/models/clnModels';
import { CLNChannelInformation } from '../../../../shared/models/alertData';
import { ADDRESS_TYPES, APICallStatusEnum, CLNActions } from '../../../../shared/services/consts-enums-functions';
import { LoggerService } from '../../../../shared/services/logger.service';
import { DataService } from '../../../../shared/services/data.service';
import { RTLState } from '../../../../store/rtl.state';
import { openSnackBar } from '../../../../store/rtl.actions';
@ -39,12 +41,32 @@ export class CLNBumpFeeComponent implements OnInit, OnDestroy {
public faInfoCircle = faInfoCircle;
public faExclamationTriangle = faExclamationTriangle;
public bumpFeeError = '';
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
public flgShowDustWarning = false;
public dustOutputValue = 0;
public recommendedFee = { fastestFee: 0, halfHourFee: 0, hourFee: 0 };
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private actions: Actions, public dialogRef: MatDialogRef<CLNBumpFeeComponent>, @Inject(MAT_DIALOG_DATA) public data: CLNChannelInformation, private store: Store<RTLState>, private logger: LoggerService, private snackBar: MatSnackBar) { }
constructor(private actions: Actions, public dialogRef: MatDialogRef<CLNBumpFeeComponent>, @Inject(MAT_DIALOG_DATA) public data: CLNChannelInformation, private store: Store<RTLState>, private logger: LoggerService, private dataService: DataService, private snackBar: MatSnackBar) { }
ngOnInit() {
this.bumpFeeChannel = this.data.channel;
this.dataService.getRecommendedFeeRates().pipe(takeUntil(this.unSubs[0])).subscribe({
next: (rfRes: RecommendedFeeRates) => {
this.recommendedFee = rfRes;
}, error: (err) => {
this.logger.error(err);
}
});
this.dataService.getBlockExplorerTransaction(this.bumpFeeChannel.funding_txid).
pipe(takeUntil(this.unSubs[1])).subscribe({
next: (txRes: BlockExplorerTransaction) => {
this.outputIndex = txRes.vout.findIndex((vout) => vout.value === this.bumpFeeChannel.to_us_msat) === 0 ? 1 : 0;
this.dustOutputValue = txRes.vout[this.outputIndex].value;
this.flgShowDustWarning = this.dustOutputValue < 1000;
}, error: (err) => {
this.logger.error(err);
}
});
}
onBumpFee(): boolean | void {
@ -69,7 +91,7 @@ export class CLNBumpFeeComponent implements OnInit, OnDestroy {
this.store.dispatch(openSnackBar({ payload: 'Successfully bumped the fee. Use the block explorer to verify transaction.' }));
this.dialogRef.close();
});
this.actions.pipe(filter((action) => action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN), takeUntil(this.unSubs[0])).
this.actions.pipe(filter((action) => action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN), takeUntil(this.unSubs[2])).
subscribe((action: any) => {
if (action.payload.status === APICallStatusEnum.ERROR && (action.payload.action === 'SetChannelTransaction' || action.payload.action === 'GenerateNewAddress')) {
this.logger.error(action.payload.message);
@ -86,6 +108,7 @@ export class CLNBumpFeeComponent implements OnInit, OnDestroy {
this.bumpFeeError = '';
this.fees = null;
this.outputIndex = null;
this.flgShowDustWarning = false;
this.outputIdx.control.setErrors(null);
}

@ -15,19 +15,21 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch">
<div fxFlex="100" class="alert alert-info">
<fa-icon class="mr-1 alert-icon" [icon]="faInfoCircle" />
<span fxLayout="column" fxFlex="100">Bumping fee on pending open channels is an advanced feature, attempt it only if you are familiar with the functionality of Bitcoin transactions.
<div>Before attempting fee bump ensure the following:</div>
<div class="pl-1">1: Use a Bitcoin block explorer to ensure that channel opening transaction is not confirmed.</div>
<div class="pl-1">2: The channel opening transaction must have a sizable change output, which can be spent further. The fee cannot be bumped without the change output.</div>
<div class="pl-1">3: Find the index value of the change output via a block explorer.</div>
<div class="pl-1">4: Enter the index value of the change output in the form below and the desired fee rate.</div>
<div class="pl-1">5: Upon successful fee bump, use your block explorer to track the child transaction in the mempool, which should be linked with the change output transaction.</div>
<span fxLayout="column" fxFlex="100">
<div>Fee rates recommended by mempool.space (sat/vByte):</div>
<div>- High: {{recommendedFee.fastestFee || 'Unknown'}}</div>
<div>- Medium: {{recommendedFee.halfHourFee || 'Unknown'}}</div>
<div>- Low: {{recommendedFee.hourFee || 'Unknown'}}</div>
</span>
</div>
<div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between center">
<div *ngIf="flgShowDustWarning" fxFlex="100" class="alert alert-warn">
<fa-icon class="mr-1 alert-icon" [icon]="faExclamationTriangle" />
<span>Change output balance <strong>{{dustOutputValue | number}}</strong> (Sats) may be insufficient for fee bumping, depending on the prevailing fee rates.</span>
</div>
<div fxLayout="row" fxFlex="100" fxLayoutAlign="space-between start">
<mat-form-field fxLayout="column" fxFlex.gt-sm="32" fxLayoutAlign="start end">
<mat-label>Index for Change Output</mat-label>
<input #outputIdx="ngModel" matInput type="number" tabindex="1" required name="outputIdx" [step]="1" [min]="0" [(ngModel)]="outputIndex">
<input #outputIdx="ngModel" autoFocus matInput type="number" tabindex="1" required name="outputIdx" [step]="1" [min]="0" [(ngModel)]="outputIndex">
<mat-error *ngIf="outputIdx.errors?.required">Index for change output is required.</mat-error>
<mat-error *ngIf="outputIdx.errors?.pendingChannelOutputIndex">Invalid index value.</mat-error>
</mat-form-field>
@ -38,16 +40,15 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="selTransType === '1'" fxFlex.gt-sm="32" fxLayoutAlign="start end">
<mat-form-field *ngIf="selTransType === '1'" fxLayout="column" fxFlex.gt-sm="32" fxLayoutAlign="start end">
<mat-label>Number of Blocks</mat-label>
<input #blcks="ngModel" matInput type="number" name="blocks" required
tabindex="3" [step]="1" [min]="0" [(ngModel)]="blocks">
<mat-error *ngIf="!blocks">Number of blocks is required.</mat-error>
</mat-form-field>
<mat-form-field *ngIf="selTransType === '2'" fxFlex.gt-sm="32" fxLayoutAlign="start end">
<mat-form-field *ngIf="selTransType === '2'" fxLayout="column" fxFlex.gt-sm="32" fxLayoutAlign="start end">
<mat-label>Fees (Sats/vByte)</mat-label>
<input #fee="ngModel" matInput
type="number" name="fees" required tabindex="4" [step]="1" [min]="0" [(ngModel)]="fees">
<input #fee="ngModel" matInput type="number" name="fees" required tabindex="4" [step]="1" [min]="0" [(ngModel)]="fees">
<mat-error *ngIf="!fees">Fees is required.</mat-error>
</mat-form-field>
</div>

@ -2,10 +2,11 @@ import { Component, OnInit, OnDestroy, ViewChild, Inject } from '@angular/core';
import { NgModel } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Subject } from 'rxjs';
import { Subject, forkJoin } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { faCopy, faInfoCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { RecommendedFeeRates, BlockExplorerTransaction } from '../../../../shared/models/rtlModels';
import { PendingOpenChannel } from '../../../../shared/models/lndModels';
import { PendingOpenChannelInformation } from '../../../../shared/models/alertData';
import { TRANS_TYPES } from '../../../../shared/services/consts-enums-functions';
@ -35,6 +36,9 @@ export class BumpFeeComponent implements OnInit, OnDestroy {
public faInfoCircle = faInfoCircle;
public faExclamationTriangle = faExclamationTriangle;
public bumpFeeError = '';
public flgShowDustWarning = false;
public dustOutputValue = 0;
public recommendedFee = { fastestFee: 0, halfHourFee: 0, hourFee: 0 };
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<BumpFeeComponent>, @Inject(MAT_DIALOG_DATA) public data: PendingOpenChannelInformation, private logger: LoggerService, private dataService: DataService, private snackBar: MatSnackBar) { }
@ -45,8 +49,25 @@ export class BumpFeeComponent implements OnInit, OnDestroy {
const channelPointArr = this.bumpFeeChannel.channel?.channel_point?.split(':') || [];
if (this.bumpFeeChannel && this.bumpFeeChannel.channel) {
this.bumpFeeChannel.channel.txid_str = channelPointArr[0] || (this.bumpFeeChannel.channel && this.bumpFeeChannel.channel.channel_point ? this.bumpFeeChannel.channel.channel_point : '');
this.bumpFeeChannel.channel.output_index = +channelPointArr[1] || null;
this.bumpFeeChannel.channel.output_index = channelPointArr[1] && channelPointArr[1] !== '' ? +channelPointArr[1] : null;
this.outputIndex = this.bumpFeeChannel.channel && this.bumpFeeChannel.channel.output_index !== null && this.bumpFeeChannel.channel.output_index === 0 ? 1 : 0;
}
this.dataService.getRecommendedFeeRates().pipe(takeUntil(this.unSubs[0])).subscribe({
next: (rfRes: RecommendedFeeRates) => {
this.recommendedFee = rfRes;
}, error: (err) => {
this.logger.error(err);
}
});
this.dataService.getBlockExplorerTransaction(this.bumpFeeChannel?.channel?.channel_point).
pipe(takeUntil(this.unSubs[1])).subscribe({
next: (txRes: BlockExplorerTransaction) => {
this.dustOutputValue = txRes.vout[this.outputIndex].value;
this.flgShowDustWarning = this.dustOutputValue < 1000;
}, error: (err) => {
this.logger.error(err);
}
});
}
onBumpFee(): boolean | void {

@ -50,10 +50,50 @@ export interface FiatCurrency {
}
export interface ConvertedCurrency {
unit: string;
iconType: 'FA' | 'SVG';
symbol: string | IconDefinition | SafeHtml;
Sats: number;
BTC: number;
OTHER: number;
unit: string;
iconType: 'FA' | 'SVG';
symbol: string | IconDefinition | SafeHtml;
Sats: number;
BTC: number;
OTHER: number;
};
export interface RecommendedFeeRates {
fastestFee: number;
halfHourFee: number;
hourFee: number;
economyFee?: number;
minimumFee?: number;
};
export interface BETransactionVOut {
value: number;
scriptpubkey?: string;
scriptpubkey_asm?: string;
scriptpubkey_type?: string;
scriptpubkey_address?: string;
}
export interface BETransactionVIn {
txid: string;
vout?: string;
prevout?: BETransactionVOut;
scriptsig?: string;
scriptsig_asm?: string;
witness?: string[];
is_coinbase?: boolean;
sequence?: number;
}
export interface BlockExplorerTransaction {
txid: string;
version?: number;
locktime?: number;
size?: number;
weight?: number;
sigops?: number;
fee?: number;
status?: any;
vin?: BETransactionVIn[];
vout?: BETransactionVOut[];
}

@ -39,6 +39,14 @@ export class DataService implements OnDestroy {
this.lnImplementationUpdated.next(this.lnImplementation);
}
getRecommendedFeeRates() {
return this.httpClient.get(API_END_POINTS.CONF_API + '/explorerFeesRecommended');
}
getBlockExplorerTransaction(txid: string) {
return this.httpClient.get(API_END_POINTS.CONF_API + '/explorerTransaction/' + txid);
}
getFiatRates() {
return this.httpClient.get(API_END_POINTS.CONF_API + '/rates');
}

@ -52,6 +52,14 @@ export class mockDataService {
this.lnImplementationUpdated.next(this.lnImplementation);
}
getRecommendedFeeRates() {
return of(mockResponseData.blockExplorerRecommendedFee);
}
getBlockExplorerTransaction(txid: string) {
return of(mockResponseData.blockExplorerTransaction);
}
getFiatRates() {
return of(mockResponseData.fiatRates);
}

@ -191,6 +191,62 @@ export const mockResponseData = {
symbol: 'TWD'
}
},
blockExplorerRecommendedFee: {
fastestFee: 19,
halfHourFee: 19,
hourFee: 19,
economyFee: 19,
minimumFee: 10
},
blockExplorerTransaction: {
txid: '44a33c87c35fb21da5140286b91b56489b9ffaf7f62bcd0cce30ab568a0129ae',
version: 1,
locktime: 0,
vin: [
{
txid: 'a41831699ce28f98e75b333268c6f2b1703a77748c49d1aa914a3c2d538077cb',
vout: 0,
prevout: {
scriptpubkey: '0014378b753631273729605c694452ff89735dca7c19',
scriptpubkey_asm: 'OP_0 OP_PUSHBYTES_20 378b753631273729605c694452ff89735dca7c19',
scriptpubkey_type: 'v0_p2wpkh',
scriptpubkey_address: 'bc1qx79h2d33yumjjczud9z99lufwdwu5lqephsey4',
value: 1436617
},
scriptsig: '',
scriptsig_asm: '',
witness: [
'3045022100936bf7c13a6b349f59b7824f98da84f271c16d4eb8c8cf4933ce8921ba45ab00022015daa05a9430a2647d7deb86e13f71e7534538e3f7b6e8af70ffaec6a7b74dda01',
'0227b8e44fb3deac91671c648707b575ce761dc35905d09a4ac1be31198ad23eef'
],
is_coinbase: false,
sequence: 4294967293
}
],
vout: [
{
scriptpubkey: '0014f8ec16db533ed1147d638e9425ad330d4d0cb1ce',
scriptpubkey_asm: 'OP_0 OP_PUSHBYTES_20 f8ec16db533ed1147d638e9425ad330d4d0cb1ce',
scriptpubkey_type: 'v0_p2wpkh',
scriptpubkey_address: 'bc1qlrkpdk6n8mg3gltr362zttfnp4xsevww8qc8md',
value: 1350761
},
{
scriptpubkey: '76a9145af54c7385fe38521bddf13cd78486a344fcae1c88ac',
scriptpubkey_asm: 'OP_DUP OP_HASH160 OP_PUSHBYTES_20 5af54c7385fe38521bddf13cd78486a344fcae1c OP_EQUALVERIFY OP_CHECKSIG',
scriptpubkey_type: 'p2pkh',
scriptpubkey_address: '19HwfuwwZUMTgvUEmoic2teLh7RU7L1HHf',
value: 84128
}
],
size: 226,
weight: 574,
sigops: 5,
fee: 1728,
status: {
confirmed: false
}
},
decodePayment: {
destination: '031844beb16bf8dd8c7bc30588b8c37b36e62b71c6e812e9b6d976c0a57e151be2',
payment_hash: 'a53968453af7ab6fc58d229a91bdf23d7c121963067f06cf02e1a7b581852c07',

Loading…
Cancel
Save