mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
Liquidity Ads Page except channel count and capacity filters
Liquidity Ads Page except channel count and capacity filters
This commit is contained in:
parent
9da3eed1b6
commit
c7fd9fad08
30 changed files with 226 additions and 128 deletions
|
|
@ -71,15 +71,14 @@ export const listNodes = (req, res, next) => {
|
|||
return res.status(options.statusCode).json({ message: options.message, error: options.error });
|
||||
}
|
||||
options.url = req.session.selectedNode.ln_server_url + '/v1/network/listNodes' + (req.query !== {} ? (JSON.stringify(req.query).replace('{', '?').replace('}', '').replace(/:/g, '=').replace(/,/g, '&').replace(/"/g, '')) : '');
|
||||
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Network', msg: 'List Nodes URL' + options.url });
|
||||
request(options).then((body) => {
|
||||
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'List Nodes Finished', data: body });
|
||||
body.forEach((node, i) => {
|
||||
node.option_will_fund.lease_fee_base_msat = (node.option_will_fund && node.option_will_fund.lease_fee_base_msat && typeof node.option_will_fund.lease_fee_base_msat === 'string' && node.option_will_fund.lease_fee_base_msat.includes('msat')) ? node.option_will_fund.lease_fee_base_msat.replace('msat', '') : node.option_will_fund.lease_fee_base_msat;
|
||||
node.option_will_fund.channel_fee_max_base_msat = (node.option_will_fund && node.option_will_fund.channel_fee_max_base_msat && typeof node.option_will_fund.channel_fee_max_base_msat === 'string' && node.option_will_fund.channel_fee_max_base_msat.includes('msat')) ? node.option_will_fund.channel_fee_max_base_msat.replace('msat', '') : node.option_will_fund.channel_fee_max_base_msat;
|
||||
// TO BE REMOVED
|
||||
node.channelCount = +(Math.random() * 10).toFixed(0);
|
||||
node.nodeCapacity = +(Math.random() * 1000000).toFixed(0);
|
||||
// TO BE REMOVED
|
||||
body.forEach((node) => {
|
||||
if (node.option_will_fund) {
|
||||
node.option_will_fund.lease_fee_base_msat = (node.option_will_fund.lease_fee_base_msat && typeof node.option_will_fund.lease_fee_base_msat === 'string' && node.option_will_fund.lease_fee_base_msat.includes('msat')) ? node.option_will_fund.lease_fee_base_msat.replace('msat', '') : node.option_will_fund.lease_fee_base_msat;
|
||||
node.option_will_fund.channel_fee_max_base_msat = (node.option_will_fund.channel_fee_max_base_msat && typeof node.option_will_fund.channel_fee_max_base_msat === 'string' && node.option_will_fund.channel_fee_max_base_msat.includes('msat')) ? node.option_will_fund.channel_fee_max_base_msat.replace('msat', '') : node.option_will_fund.channel_fee_max_base_msat;
|
||||
}
|
||||
return node;
|
||||
});
|
||||
res.status(200).json(body);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
1
frontend/564.d41ab2b8bdd42ac9.js
Normal file
1
frontend/564.d41ab2b8bdd42ac9.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/636.f8c5bc0215761948.js
Normal file
1
frontend/636.f8c5bc0215761948.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/924.ff524ffae95c311b.js
Normal file
1
frontend/924.ff524ffae95c311b.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -13,6 +13,6 @@
|
|||
<style>@font-face{font-family:Roboto;src:url(Roboto-Thin.f7a95c9c5999532c.woff2) format("woff2"),url(Roboto-Thin.c13c157cb81e8ebb.woff) format("woff");font-weight:100;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.b0e084abf689f393.woff2) format("woff2"),url(Roboto-ThinItalic.1111028df6cea564.woff) format("woff");font-weight:100;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Light.0e01b6cd13b3857f.woff2) format("woff2"),url(Roboto-Light.603ca9a537b88428.woff) format("woff");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.232ef4b20215f720.woff2) format("woff2"),url(Roboto-LightItalic.1b5e142f787151c8.woff) format("woff");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Regular.475ba9e4e2d63456.woff2) format("woff2"),url(Roboto-Regular.bcefbfee882bc1cb.woff) format("woff");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.e3a9ebdaac06bbc4.woff2) format("woff2"),url(Roboto-RegularItalic.0668fae6af0cf8c2.woff) format("woff");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Medium.457532032ceb0168.woff2) format("woff2"),url(Roboto-Medium.6e1ae5f0b324a0aa.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.872f7060602d55d2.woff2) format("woff2"),url(Roboto-MediumItalic.e06fb533801cbb08.woff) format("woff");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Bold.447291a88c067396.woff2) format("woff2"),url(Roboto-Bold.fc482e6133cf5e26.woff) format("woff");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.1b15168ef6fa4e16.woff2) format("woff2"),url(Roboto-BoldItalic.e26ba339b06f09f7.woff) format("woff");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Black.2eaa390d458c877d.woff2) format("woff2"),url(Roboto-Black.b25f67ad8583da68.woff) format("woff");font-weight:900;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.7dc03ee444552bc5.woff2) format("woff2"),url(Roboto-BlackItalic.c8dc642467cb3099.woff) format("woff");font-weight:900;font-style:italic}html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:62.5%}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}</style><link rel="stylesheet" href="styles.2872d180f488fe2c.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.2872d180f488fe2c.css"></noscript></head>
|
||||
<body>
|
||||
<rtl-app></rtl-app>
|
||||
<script src="runtime.ecf038df3dc1dad4.js" type="module"></script><script src="polyfills.c0773154203456c6.js" type="module"></script><script src="main.26417b52daa70815.js" type="module"></script>
|
||||
<script src="runtime.dae90b7a05c6a024.js" type="module"></script><script src="polyfills.c0773154203456c6.js" type="module"></script><script src="main.1300125d72833537.js" type="module"></script>
|
||||
|
||||
</body></html>
|
||||
1
frontend/main.1300125d72833537.js
Normal file
1
frontend/main.1300125d72833537.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
(()=>{"use strict";var e,v={},g={};function r(e){var n=g[e];if(void 0!==n)return n.exports;var t=g[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=(n,t,f,o)=>{if(!t){var a=1/0;for(i=0;i<e.length;i++){for(var[t,f,o]=e[i],c=!0,u=0;u<t.length;u++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[u]))?t.splice(u--,1):(c=!1,o<a&&(a=o));if(c){e.splice(i--,1);var d=f();void 0!==d&&(n=d)}}return n}o=o||0;for(var i=e.length;i>0&&e[i-1][2]>o;i--)e[i]=e[i-1];e[i]=[t,f,o]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>e+"."+{404:"ecea7accd2beec12",636:"7f276612476ed267",893:"355edadcb4ce94f4",924:"ebace471eb0d0c1c"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="RTLApp:";r.l=(t,f,o,i)=>{if(e[t])e[t].push(f);else{var a,c;if(void 0!==o)for(var u=document.getElementsByTagName("script"),d=0;d<u.length;d++){var l=u[d];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==n+o){a=l;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",n+o),a.src=r.tu(t)),e[t]=[f];var s=(m,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(_=>_(b)),m)return m(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=>{"undefined"!=typeof Symbol&&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:n=>n},"undefined"!=typeof trustedTypes&&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=(f,o)=>{var i=r.o(e,f)?e[f]:void 0;if(0!==i)if(i)o.push(i[2]);else if(666!=f){var a=new Promise((l,s)=>i=e[f]=[l,s]);o.push(i[2]=a);var c=r.p+r.u(f),u=new Error;r.l(c,l=>{if(r.o(e,f)&&(0!==(i=e[f])&&(e[f]=void 0),i)){var s=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;u.message="Loading chunk "+f+" failed.\n("+s+": "+p+")",u.name="ChunkLoadError",u.type=s,u.request=p,i[1](u)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var n=(f,o)=>{var u,d,[i,a,c]=o,l=0;if(i.some(p=>0!==e[p])){for(u in a)r.o(a,u)&&(r.m[u]=a[u]);if(c)var s=c(r)}for(f&&f(o);l<i.length;l++)r.o(e,d=i[l])&&e[d]&&e[d][0](),e[d]=0;return r.O(s)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})();
|
||||
(()=>{"use strict";var e,v={},g={};function r(e){var n=g[e];if(void 0!==n)return n.exports;var t=g[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=(n,t,f,o)=>{if(!t){var a=1/0;for(i=0;i<e.length;i++){for(var[t,f,o]=e[i],s=!0,u=0;u<t.length;u++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[u]))?t.splice(u--,1):(s=!1,o<a&&(a=o));if(s){e.splice(i--,1);var d=f();void 0!==d&&(n=d)}}return n}o=o||0;for(var i=e.length;i>0&&e[i-1][2]>o;i--)e[i]=e[i-1];e[i]=[t,f,o]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>e+"."+{564:"d41ab2b8bdd42ac9",636:"f8c5bc0215761948",893:"355edadcb4ce94f4",924:"ff524ffae95c311b"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="RTLApp:";r.l=(t,f,o,i)=>{if(e[t])e[t].push(f);else{var a,s;if(void 0!==o)for(var u=document.getElementsByTagName("script"),d=0;d<u.length;d++){var l=u[d];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==n+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",n+o),a.src=r.tu(t)),e[t]=[f];var c=(m,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(_=>_(b)),m)return m(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=>{"undefined"!=typeof Symbol&&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:n=>n},"undefined"!=typeof trustedTypes&&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=(f,o)=>{var i=r.o(e,f)?e[f]:void 0;if(0!==i)if(i)o.push(i[2]);else if(666!=f){var a=new Promise((l,c)=>i=e[f]=[l,c]);o.push(i[2]=a);var s=r.p+r.u(f),u=new Error;r.l(s,l=>{if(r.o(e,f)&&(0!==(i=e[f])&&(e[f]=void 0),i)){var c=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;u.message="Loading chunk "+f+" failed.\n("+c+": "+p+")",u.name="ChunkLoadError",u.type=c,u.request=p,i[1](u)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var n=(f,o)=>{var u,d,[i,a,s]=o,l=0;if(i.some(p=>0!==e[p])){for(u in a)r.o(a,u)&&(r.m[u]=a[u]);if(s)var c=s(r)}for(f&&f(o);l<i.length;l++)r.o(e,d=i[l])&&e[d]&&e[d][0](),e[d]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})();
|
||||
|
|
@ -66,15 +66,14 @@ export const listNodes = (req, res, next) => {
|
|||
options = common.getOptions(req);
|
||||
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
|
||||
options.url = req.session.selectedNode.ln_server_url + '/v1/network/listNodes' + (req.query !== {} ? (JSON.stringify(req.query).replace('{', '?').replace('}', '').replace(/:/g, '=').replace(/,/g, '&').replace(/"/g, '')) : '');
|
||||
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Network', msg: 'List Nodes URL' + options.url });
|
||||
request(options).then((body) => {
|
||||
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Network', msg: 'List Nodes Finished', data: body });
|
||||
body.forEach((node, i) => {
|
||||
node.option_will_fund.lease_fee_base_msat = (node.option_will_fund && node.option_will_fund.lease_fee_base_msat && typeof node.option_will_fund.lease_fee_base_msat === 'string' && node.option_will_fund.lease_fee_base_msat.includes('msat')) ? node.option_will_fund.lease_fee_base_msat.replace('msat', '') : node.option_will_fund.lease_fee_base_msat;
|
||||
node.option_will_fund.channel_fee_max_base_msat = (node.option_will_fund && node.option_will_fund.channel_fee_max_base_msat && typeof node.option_will_fund.channel_fee_max_base_msat === 'string' && node.option_will_fund.channel_fee_max_base_msat.includes('msat')) ? node.option_will_fund.channel_fee_max_base_msat.replace('msat', '') : node.option_will_fund.channel_fee_max_base_msat;
|
||||
// TO BE REMOVED
|
||||
node.channelCount = +(Math.random() * 10).toFixed(0);
|
||||
node.nodeCapacity = +(Math.random() * 1000000).toFixed(0);
|
||||
// TO BE REMOVED
|
||||
body.forEach((node) => {
|
||||
if (node.option_will_fund) {
|
||||
node.option_will_fund.lease_fee_base_msat = (node.option_will_fund.lease_fee_base_msat && typeof node.option_will_fund.lease_fee_base_msat === 'string' && node.option_will_fund.lease_fee_base_msat.includes('msat')) ? node.option_will_fund.lease_fee_base_msat.replace('msat', '') : node.option_will_fund.lease_fee_base_msat;
|
||||
node.option_will_fund.channel_fee_max_base_msat = (node.option_will_fund.channel_fee_max_base_msat && typeof node.option_will_fund.channel_fee_max_base_msat === 'string' && node.option_will_fund.channel_fee_max_base_msat.includes('msat')) ? node.option_will_fund.channel_fee_max_base_msat.replace('msat', '') : node.option_will_fund.channel_fee_max_base_msat;
|
||||
}
|
||||
return node;
|
||||
});
|
||||
res.status(200).json(body);
|
||||
|
|
|
|||
|
|
@ -19,21 +19,17 @@
|
|||
</span>
|
||||
</div>
|
||||
<mat-form-field fxFlex="34">
|
||||
<input autoFocus matInput placeholder="Channel Amount (Sats)" name="channelAmount" [(ngModel)]="channelAmount" tabindex="1" type="number" step="1000" required>
|
||||
<input autoFocus matInput placeholder="Channel Amount (Sats)" name="channelAmount" [(ngModel)]="channelAmount" (keyup)="onCalculateOpeningFee()" tabindex="1" type="number" step="10000" required>
|
||||
<mat-error *ngIf="!channelAmount">Channel amount is required.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="34">
|
||||
<input matInput placeholder="Channel Opening Fee Rate (Sats/vByte)" name="channelOpeningFeeRate" [(ngModel)]="channelOpeningFeeRate" type="number" step="1" tabindex="2" required>
|
||||
<input matInput placeholder="Channel Opening Fee Rate (Sats/vByte)" name="channelOpeningFeeRate" [(ngModel)]="channelOpeningFeeRate" (keyup)="onCalculateOpeningFee()" type="number" step="10" tabindex="2" required>
|
||||
<mat-error *ngIf="!channelOpeningFeeRate">Channel opening fee rate is required.</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div fxLayout="row" class="my-1">
|
||||
<button class="mr-1" mat-stroked-button color="primary" tabindex="3" type="reset" (click)="onReset()">Clear</button>
|
||||
<button mat-flat-button color="primary" (click)="onRecalculate()" tabindex="4" type="submit">Recalculate</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-divider [inset]="true" class="my-2"></mat-divider>
|
||||
<form fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-sm="row wrap" #formFilter="ngForm">
|
||||
<!-- <mat-divider [inset]="true" class="my-2"></mat-divider> -->
|
||||
<!-- <form fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-sm="row wrap" #formFilter="ngForm">
|
||||
<div fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign.gt-sm="space-between center" fxLayoutAlign="start start" class="page-sub-title-container mt-1">
|
||||
<div fxFlex="30">
|
||||
<span class="page-title">
|
||||
|
|
@ -49,8 +45,8 @@
|
|||
<input matInput placeholder="Channel Count" name="channelCount" [(ngModel)]="channelCount" (keyup)="onFilter()" type="number" step="1" min="0" tabindex="6">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
<div [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container">
|
||||
</form> -->
|
||||
<div [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container mt-2">
|
||||
<mat-progress-bar *ngIf="apiCallStatus.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
|
||||
<table mat-table #table [dataSource]="liquidityNodes" matSort [ngClass]="{'overflow-auto error-border': errorMessage !== '','overflow-auto': true}">
|
||||
<ng-container matColumnDef="alias">
|
||||
|
|
@ -59,30 +55,31 @@
|
|||
{{lqNode?.alias}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="capacity">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Capacity </th>
|
||||
<td mat-cell *matCellDef="let lqNode"><span fxLayoutAlign="end center">
|
||||
{{lqNode?.option_will_fund?.lease_fee_basis | number:'1.0-0'}} </span></td>
|
||||
<ng-container matColumnDef="capacityChannels">
|
||||
<th mat-header-cell *matHeaderCellDef> Capacity/Channels </th>
|
||||
<td mat-cell *matCellDef="let lqNode">
|
||||
{{lqNode?.nodeCapacity/100000000 | number:'1.0-2'}} BTC / {{lqNode?.channelCount | number:'1.0-0'}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="numChannels">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Channels </th>
|
||||
<td mat-cell *matCellDef="let lqNode"><span fxLayoutAlign="end center">
|
||||
{{lqNode?.option_will_fund?.lease_fee_basis | number:'1.0-0'}} </span></td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="leaseFeeBasis">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Lease Fee </th>
|
||||
<td mat-cell *matCellDef="let lqNode"><span fxLayoutAlign="end center">
|
||||
{{lqNode?.option_will_fund?.lease_fee_basis | number:'1.0-0'}} </span></td>
|
||||
<ng-container matColumnDef="leaseFee">
|
||||
<th mat-header-cell *matHeaderCellDef> Lease Fee </th>
|
||||
<td mat-cell *matCellDef="let lqNode">
|
||||
{{lqNode?.option_will_fund?.lease_fee_base_msat/1000 | number:'1.0-0'}} Sats + {{(lqNode?.option_will_fund?.lease_fee_basis/100) | number:'1.2-2'}}%
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="routingFee">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Routing Fee </th>
|
||||
<td mat-cell *matCellDef="let lqNode"><span fxLayoutAlign="end center">
|
||||
{{lqNode?.option_will_fund?.lease_fee_basis | number:'1.0-0'}} </span></td>
|
||||
<th mat-header-cell *matHeaderCellDef> Routing Fee </th>
|
||||
<td mat-cell *matCellDef="let lqNode">
|
||||
{{lqNode?.option_will_fund?.channel_fee_max_base_msat/1000 | number:'1.0-0'}} Sats + {{lqNode?.option_will_fund?.channel_fee_max_proportional_thousandths * 1000 | number:'1.0-0'}} ppm
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="channelOpenFee">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Channel Open Fee </th>
|
||||
<td mat-cell *matCellDef="let lqNode"><span fxLayoutAlign="end center">
|
||||
{{lqNode?.option_will_fund?.lease_fee_basis | number:'1.0-0'}} </span></td>
|
||||
<ng-container matColumnDef="channelOpeningFee">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Channel Opening Fee </th>
|
||||
<td mat-cell *matCellDef="let lqNode">
|
||||
<span fxLayoutAlign="end center">
|
||||
{{lqNode.channelOpeningFee | number:'1.0-0'}} Sats
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef class="px-3">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
|
||||
import { RootReducer } from '../../../store/rtl.reducers';
|
||||
import { LNDReducer } from '../../../lnd/store/lnd.reducers';
|
||||
import { CLNReducer } from '../../../cln/store/cln.reducers';
|
||||
import { ECLReducer } from '../../../eclair/store/ecl.reducers';
|
||||
import { DataService } from '../../../shared/services/data.service';
|
||||
import { CommonService } from '../../../shared/services/common.service';
|
||||
import { mockDataService } from '../../../shared/test-helpers/mock-services';
|
||||
import { RTLEffects } from '../../../store/rtl.effects';
|
||||
import { mockCLEffects, mockDataService, mockECLEffects, mockLNDEffects, mockRTLEffects } from '../../../shared/test-helpers/mock-services';
|
||||
|
||||
import { CLNLiquidityAdsListComponent } from './liquidity-ads-list.component';
|
||||
|
||||
|
|
@ -17,11 +24,14 @@ describe('CLNLiquidityAdsListComponent', () => {
|
|||
declarations: [CLNLiquidityAdsListComponent],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
SharedModule
|
||||
SharedModule,
|
||||
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer }),
|
||||
EffectsModule.forRoot([mockRTLEffects, mockLNDEffects, mockCLEffects, mockECLEffects])
|
||||
],
|
||||
providers: [
|
||||
CommonService,
|
||||
{ provide: DataService, useClass: mockDataService }
|
||||
{ provide: DataService, useClass: mockDataService },
|
||||
{ provide: RTLEffects, useClass: mockRTLEffects }
|
||||
]
|
||||
}).
|
||||
compileComponents();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Subject, combineLatest } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
|
|
@ -11,14 +11,14 @@ import { DataService } from '../../../shared/services/data.service';
|
|||
import { LoggerService } from '../../../shared/services/logger.service';
|
||||
import { CommonService } from '../../../shared/services/common.service';
|
||||
import { AlertTypeEnum, APICallStatusEnum, DataTypeEnum, getPaginatorLabel, PAGE_SIZE, PAGE_SIZE_OPTIONS, ScreenSizeEnum } from '../../../shared/services/consts-enums-functions';
|
||||
import { Balance, GetInfo, LookupNode } from '../../../shared/models/clnModels';
|
||||
import { GetInfo, LookupNode } from '../../../shared/models/clnModels';
|
||||
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
|
||||
import { openAlert, openConfirmation } from '../../../store/rtl.actions';
|
||||
|
||||
import { RTLState } from '../../../store/rtl.state';
|
||||
import { RTLEffects } from '../../../store/rtl.effects';
|
||||
import { CLNOpenLiquidityChannelComponent } from '../open-liquidity-channel-modal/open-liquidity-channel-modal.component';
|
||||
import { nodeInfoAndNodeSettingsAndBalance } from '../../store/cln.selector';
|
||||
import { SelNodeChild } from '../../../shared/models/RTLconfig';
|
||||
|
||||
@Component({
|
||||
selector: 'rtl-cln-liquidity-ads-list',
|
||||
|
|
@ -40,7 +40,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
|
|||
public totalBalance = 0;
|
||||
public information: GetInfo;
|
||||
public channelAmount = 100000;
|
||||
public channelOpeningFeeRate = 2;
|
||||
public channelOpeningFeeRate = 10;
|
||||
public nodeCapacity = 500000;
|
||||
public channelCount = 5;
|
||||
public liquidityNodesData: LookupNode[] = [];
|
||||
|
|
@ -62,55 +62,64 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
|
|||
this.screenSize = this.commonService.getScreenSize();
|
||||
if (this.screenSize === ScreenSizeEnum.XS) {
|
||||
this.flgSticky = false;
|
||||
this.displayedColumns = ['alias', 'capacity', 'actions'];
|
||||
this.displayedColumns = ['alias', 'channelOpeningFee', 'actions'];
|
||||
} else if (this.screenSize === ScreenSizeEnum.SM) {
|
||||
this.flgSticky = false;
|
||||
this.displayedColumns = ['alias', 'capacity', 'numChannels', 'leaseFeeBasis', 'routingFee', 'channelOpenFee', 'actions'];
|
||||
this.displayedColumns = ['alias', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
|
||||
// this.displayedColumns = ['alias', 'capacityChannels', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
|
||||
} else if (this.screenSize === ScreenSizeEnum.MD) {
|
||||
this.flgSticky = false;
|
||||
this.displayedColumns = ['alias', 'capacity', 'numChannels', 'leaseFeeBasis', 'routingFee', 'channelOpenFee', 'actions'];
|
||||
this.displayedColumns = ['alias', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
|
||||
// this.displayedColumns = ['alias', 'capacityChannels', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
|
||||
} else {
|
||||
this.flgSticky = true;
|
||||
this.displayedColumns = ['alias', 'capacity', 'numChannels', 'leaseFeeBasis', 'routingFee', 'channelOpenFee', 'actions'];
|
||||
this.displayedColumns = ['alias', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
|
||||
// this.displayedColumns = ['alias', 'capacityChannels', 'leaseFee', 'routingFee', 'channelOpeningFee', 'actions'];
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dataService.listNetworkNodes('?liquidity_ads=yes').pipe(takeUntil(this.unSubs[0])).subscribe({
|
||||
next: (res: any) => {
|
||||
this.logger.info('Received Liquidity Ads Enabled Nodes: ' + JSON.stringify(res));
|
||||
this.apiCallStatus.status = APICallStatusEnum.COMPLETED;
|
||||
this.liquidityNodesData = res;
|
||||
this.loadLiqNodesTable(this.liquidityNodesData);
|
||||
}, error: (err) => {
|
||||
this.logger.error('Liquidity Ads Nodes Error: ' + JSON.stringify(err));
|
||||
this.apiCallStatus.status = APICallStatusEnum.ERROR;
|
||||
this.errorMessage = JSON.stringify(err);
|
||||
}
|
||||
});
|
||||
this.store.select(nodeInfoAndNodeSettingsAndBalance).pipe(takeUntil(this.unSubs[1])).
|
||||
subscribe((infoSettingsBalSelector: { information: GetInfo, nodeSettings: SelNodeChild, balance: Balance }) => {
|
||||
this.information = infoSettingsBalSelector.information;
|
||||
this.totalBalance = infoSettingsBalSelector.balance.totalBalance;
|
||||
this.logger.info(infoSettingsBalSelector);
|
||||
combineLatest([this.store.select(nodeInfoAndNodeSettingsAndBalance), this.dataService.listNetworkNodes('?liquidity_ads=yes')]).pipe(takeUntil(this.unSubs[0])).
|
||||
subscribe({
|
||||
next: ([infoSettingsBalSelector, nodeListRes]) => {
|
||||
this.information = infoSettingsBalSelector.information;
|
||||
this.totalBalance = infoSettingsBalSelector.balance.totalBalance;
|
||||
this.logger.info(infoSettingsBalSelector);
|
||||
if (nodeListRes && !(<any[]>nodeListRes).length) { nodeListRes = []; }
|
||||
this.logger.info('Received Liquidity Ads Enabled Nodes: ' + JSON.stringify(nodeListRes));
|
||||
this.apiCallStatus.status = APICallStatusEnum.COMPLETED;
|
||||
this.liquidityNodesData = (<LookupNode[]>nodeListRes).filter((node) => node.nodeid !== this.information.id);
|
||||
this.onCalculateOpeningFee();
|
||||
this.loadLiqNodesTable(this.liquidityNodesData);
|
||||
}, error: (err) => {
|
||||
this.logger.error('Liquidity Ads Nodes Error: ' + JSON.stringify(err));
|
||||
this.apiCallStatus.status = APICallStatusEnum.ERROR;
|
||||
this.errorMessage = JSON.stringify(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onRecalculate() {
|
||||
|
||||
onCalculateOpeningFee() {
|
||||
this.liquidityNodesData.forEach((lqNode) => {
|
||||
if (lqNode.option_will_fund) {
|
||||
lqNode.channelOpeningFee = (+(lqNode.option_will_fund.lease_fee_base_msat) / 1000) + (this.channelAmount * (+lqNode.option_will_fund.lease_fee_basis) / 10000) + ((+lqNode.option_will_fund.funding_weight / 4) * this.channelOpeningFeeRate);
|
||||
}
|
||||
});
|
||||
if (this.paginator) { this.paginator.firstPage(); }
|
||||
}
|
||||
|
||||
onFilter() {
|
||||
this.liquidityNodes.filter = 'Changed';
|
||||
}
|
||||
// onFilter() {
|
||||
// this.liquidityNodes.filter = 'Changed';
|
||||
// }
|
||||
|
||||
loadLiqNodesTable(liqNodes: LookupNode[]) {
|
||||
this.liquidityNodes = new MatTableDataSource<LookupNode>([...liqNodes]);
|
||||
this.liquidityNodes.sortingDataAccessor = (data: any, sortHeaderId: string) => ((data[sortHeaderId] && isNaN(data[sortHeaderId])) ? data[sortHeaderId].toLocaleLowerCase() : data[sortHeaderId] ? +data[sortHeaderId] : null);
|
||||
this.liquidityNodes.sort = this.sort;
|
||||
this.liquidityNodes.filterPredicate = (node: LookupNode, fltr: string) => node.channelCount >= this.channelCount && node.nodeCapacity >= this.nodeCapacity;
|
||||
this.liquidityNodes.paginator = this.paginator;
|
||||
this.onFilter();
|
||||
if (this.sort) { this.sort.sort({ id: 'channelOpeningFee', start: 'asc', disableClear: true }); }
|
||||
// this.liquidityNodes.filterPredicate = (node: LookupNode, fltr: string) => node.channelCount >= this.channelCount && node.nodeCapacity >= this.nodeCapacity;
|
||||
// this.onFilter();
|
||||
}
|
||||
|
||||
onOpenChannel(lqNode: LookupNode) {
|
||||
|
|
@ -143,10 +152,9 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
|
|||
[{ key: 'nodeid', value: lqNode.nodeid, title: 'Node ID', width: 100, type: DataTypeEnum.STRING }],
|
||||
[{ key: 'base_fee', value: (lqNode.option_will_fund.lease_fee_base_msat / 1000), title: 'Lease Base Fee (Sats)', width: 50, type: DataTypeEnum.NUMBER },
|
||||
{ key: 'fee_basis', value: lqNode.option_will_fund.lease_fee_basis, title: 'Lease Base Basis (bps)', width: 50, type: DataTypeEnum.NUMBER }],
|
||||
[{ key: 'lease_fee_rate', value: '-000', title: 'Lease Fee Rate (ppm)', width: 50, type: DataTypeEnum.NUMBER },
|
||||
{ key: 'funding_rate', value: lqNode.option_will_fund.funding_weight, title: 'Channel Open Funding Rate', width: 50, type: DataTypeEnum.NUMBER }],
|
||||
[{ key: 'channel_max_base', value: (lqNode.option_will_fund.channel_fee_max_base_msat / 1000), title: 'Max Channel Routing Base Fee (Sats)', width: 50, type: DataTypeEnum.NUMBER },
|
||||
{ key: 'channel_max_rate', value: lqNode.option_will_fund.channel_fee_max_proportional_thousandths, title: 'Max Channel Routing Fee Rate (ppm)', width: 50, type: DataTypeEnum.NUMBER }],
|
||||
[{ key: 'channel_max_base', value: lqNode.option_will_fund.channel_fee_max_base_msat / 1000, title: 'Max Channel Routing Base Fee (Sats)', width: 50, type: DataTypeEnum.NUMBER },
|
||||
{ key: 'channel_max_rate', value: lqNode.option_will_fund.channel_fee_max_proportional_thousandths * 1000, title: 'Max Channel Routing Fee Rate (ppm)', width: 50, type: DataTypeEnum.NUMBER }],
|
||||
[{ key: 'funding_rate', value: lqNode.option_will_fund.funding_weight, title: 'Funding Weight', width: 100, type: DataTypeEnum.NUMBER }],
|
||||
[{ key: 'address', value: addArr, title: 'Address', width: 100, type: DataTypeEnum.ARRAY }]
|
||||
];
|
||||
this.store.dispatch(openConfirmation({
|
||||
|
|
@ -160,7 +168,7 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
}));
|
||||
this.rtlEffects.closeConfirm.pipe(takeUntil(this.unSubs[2])).subscribe((confirmRes) => {
|
||||
this.rtlEffects.closeConfirm.pipe(takeUntil(this.unSubs[1])).subscribe((confirmRes) => {
|
||||
if (confirmRes) {
|
||||
this.onOpenChannel(lqNode);
|
||||
}
|
||||
|
|
@ -173,11 +181,6 @@ export class CLNLiquidityAdsListComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.channelAmount = 0;
|
||||
this.channelOpeningFeeRate = 0;
|
||||
}
|
||||
|
||||
onFilterReset() {
|
||||
this.nodeCapacity = 0;
|
||||
this.channelCount = 0;
|
||||
|
|
|
|||
|
|
@ -11,17 +11,17 @@
|
|||
<ng-container *ngTemplateOutlet="nodeDetailsExpansionBlock"></ng-container>
|
||||
<div fxLayout="column" fxLayoutAlign="space-between stretch" fxLayoutAlign.gt-sm="space-between center" fxLayout.gt-sm="row wrap">
|
||||
<mat-form-field fxFlex="30" fxLayoutAlign="start end">
|
||||
<input autoFocus matInput [(ngModel)]="requestedAmount" placeholder="Requested Amount" type="number" [step]="1000" [min]="0" tabindex="1" required name="ramount" #ramount="ngModel">
|
||||
<input autoFocus matInput [(ngModel)]="requestedAmount" (keyup)="calculateFee()" placeholder="Requested Amount" type="number" [step]="10000" [min]="0" tabindex="1" required name="ramount" #ramount="ngModel">
|
||||
<span matSuffix> Sats </span>
|
||||
<mat-error *ngIf="ramount.errors?.required">Requested amount is required.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30" fxLayoutAlign="start end">
|
||||
<input matInput [(ngModel)]="feeRate" placeholder="Fee Rate" type="number" [step]="1" [min]="0" tabindex="2" required name="feerate" #feeRt="ngModel">
|
||||
<input matInput [(ngModel)]="feeRate" placeholder="Fee Rate" (keyup)="calculateFee()" type="number" [step]="10" [min]="0" tabindex="2" required name="feerate" #feeRt="ngModel">
|
||||
<span matSuffix> Sats/vByte </span>
|
||||
<mat-error *ngIf="feeRt.errors?.required">Fee rate is required.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30" fxLayoutAlign="start end">
|
||||
<input matInput [(ngModel)]="localAmount" placeholder="Local Amount" type="number" [step]="1000" [min]="20000" [max]="totalBalance" tabindex="3" required name="lamount" #lamount="ngModel">
|
||||
<input matInput [(ngModel)]="localAmount" placeholder="Local Amount" type="number" [step]="10000" [min]="20000" [max]="totalBalance" tabindex="3" required name="lamount" #lamount="ngModel">
|
||||
<mat-hint>Remaining Bal: {{totalBalance - ((localAmount) ? localAmount : 0) | number}}</mat-hint>
|
||||
<span matSuffix> Sats </span>
|
||||
<mat-error *ngIf="lamount.errors?.required">Local amount is required.</mat-error>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
<div fxFlex="100" class="alert alert-info mt-2">
|
||||
<span>Total cost to lease {{1250 | number}} (Sats)</span>
|
||||
<span>Total cost to lease {{node.channelOpeningFee | number}} (Sats)</span>
|
||||
</div>
|
||||
<div fxFlex="100" class="alert alert-danger mt-2" *ngIf="channelConnectionError !== ''">
|
||||
<fa-icon [icon]="faExclamationTriangle" class="mr-1 alert-icon"></fa-icon>
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
</div>
|
||||
|
||||
<ng-template #nodeDetailsExpansionBlock>
|
||||
<mat-expansion-panel class="flat-expansion-panel my-1" *ngIf="node" expanded="false">
|
||||
<mat-expansion-panel class="flat-expansion-panel mt-1 mb-2" *ngIf="node" expanded="false">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<span>Node: </span><strong class="font-weight-900">{{node?.alias || node?.nodeid}}</strong>
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
<div fxLayout="row">
|
||||
<div fxFlex="100">
|
||||
<h4 fxLayoutAlign="start" class="font-bold-500">Last Timestamp</h4>
|
||||
<span class="overflow-wrap foreground-secondary-text">{{node.last_timestamp}}</span>
|
||||
<span class="overflow-wrap foreground-secondary-text">{{(node.last_timestamp * 1000) | date:'dd/MMM/y HH:mm'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider class="w-100 my-1"></mat-divider>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,36 @@ describe('CLNOpenLiquidityChannelComponent', () => {
|
|||
CommonService,
|
||||
{ provide: DataService, useClass: mockDataService },
|
||||
{ provide: MatDialogRef, useClass: mockMatDialogRef },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { message: {} } }
|
||||
{
|
||||
provide: MAT_DIALOG_DATA, useValue: {
|
||||
message: {
|
||||
node: {
|
||||
nodeid: '0263983b2261d6ad9e2e134c1aa60d0c82d47c1c16b876a096a38d4f283fc236dc',
|
||||
alias: 'blyte c-lightning',
|
||||
color: '026398',
|
||||
last_timestamp: 1640062643,
|
||||
features: '800088282a6aa2',
|
||||
addresses: [
|
||||
{
|
||||
type: 'ipv4',
|
||||
address: '172.105.98.46',
|
||||
port: 9747
|
||||
}
|
||||
],
|
||||
option_will_fund: {
|
||||
lease_fee_base_msat: '20000000',
|
||||
lease_fee_basis: 50,
|
||||
funding_weight: 666,
|
||||
channel_fee_max_base_msat: '0',
|
||||
channel_fee_max_proportional_thousandths: 1,
|
||||
compact_lease: '029a0032000100004e20'
|
||||
},
|
||||
channelOpeningFee: 22165
|
||||
},
|
||||
balance: 100000, requestedAmount: 20000, feeRate: 10, localAmount: 20000
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}).
|
||||
compileComponents();
|
||||
|
|
|
|||
|
|
@ -63,14 +63,19 @@ export class CLNOpenLiquidityChannelComponent implements OnInit, OnDestroy {
|
|||
this.form.controls.ramount.setValue(this.data.message.requestedAmount);
|
||||
this.form.controls.feerate.setValue(this.data.message.feeRate);
|
||||
this.form.controls.lamount.setValue(this.data.message.localAmount);
|
||||
this.calculateFee();
|
||||
this.channelConnectionError = '';
|
||||
}
|
||||
|
||||
calculateFee() {
|
||||
this.node.channelOpeningFee = (+(this.node.option_will_fund.lease_fee_base_msat) / 1000) + (this.requestedAmount * (+this.node.option_will_fund.lease_fee_basis) / 10000) + ((+this.node.option_will_fund.funding_weight / 4) * this.feeRate);
|
||||
}
|
||||
|
||||
onOpenChannel(): boolean | void {
|
||||
if (!this.node || !this.requestedAmount || !this.feeRate || !this.localAmount) {
|
||||
if (!this.node || !this.node.option_will_fund || !this.requestedAmount || !this.feeRate || !this.localAmount) {
|
||||
return true;
|
||||
}
|
||||
const newChannel = { peerId: this.node.nodeid, satoshis: this.requestedAmount.toString(), announce: false, feeRate: this.feeRate + 'perkb' };
|
||||
const newChannel = { peerId: this.node.nodeid, satoshis: this.localAmount.toString(), feeRate: this.feeRate + 'perkb', requestAmount: this.requestedAmount.toString(), compactLease: this.node.option_will_fund.compact_lease };
|
||||
this.store.dispatch(saveNewChannel({ payload: newChannel }));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="selFeeRate === 'customperkb' && !flgMinConf" fxFlex="48" fxLayoutAlign="end center">
|
||||
<input matInput [(ngModel)]="customFeeRate" placeholder="Fee Rate (Sats/vB)" type="number" name="custFeeRate" [step]="0.1" [min]="0" tabindex="4" #custFeeRate="ngModel" [required]="selFeeRate === 'customperkb' && !flgMinConf">
|
||||
<input matInput [(ngModel)]="customFeeRate" placeholder="Fee Rate (Sats/vByte)" type="number" name="custFeeRate" [step]="0.1" [min]="0" tabindex="4" #custFeeRate="ngModel" [required]="selFeeRate === 'customperkb' && !flgMinConf">
|
||||
<mat-error *ngIf="selFeeRate === 'customperkb' && !flgMinConf && !customFeeRate">Fee Rate is required.</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="sendFundFormGroup.controls.selFeeRate.value === 'customperkb' && !sendFundFormGroup.controls.flgMinConf.value" fxFlex="48" fxLayoutAlign="end center">
|
||||
<input matInput formControlName="customFeeRate" placeholder="Fee Rate (Sats/vB)" type="number" name="custFeeRate" [step]="0.1" [min]="0" tabindex="4">
|
||||
<input matInput formControlName="customFeeRate" placeholder="Fee Rate (Sats/vByte)" type="number" name="custFeeRate" [step]="0.1" [min]="0" tabindex="4">
|
||||
<mat-error *ngIf="sendFundFormGroup.controls.selFeeRate.value === 'customperkb' && !sendFundFormGroup.controls.flgMinConf.value && !sendFundFormGroup.controls.customFeeRate.value">Fee Rate is required.</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
</mat-expansion-panel-header>
|
||||
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch">
|
||||
<div fxLayout="column" fxLayoutAlign="space-between stretch" fxLayoutAlign.gt-sm="space-between center" fxLayout.gt-sm="row wrap">
|
||||
<div fxFlex="48" fxLayoutAlign="space-between end">
|
||||
<div fxFlex="54" fxLayoutAlign="space-between end">
|
||||
<mat-form-field [fxFlex]="selFeeRate === 'customperkb' && !flgMinConf ? '48' : '100'" fxLayoutAlign="start center">
|
||||
<mat-select tabindex="4" placeholder="Fee Rate" [(value)]="selFeeRate" (selectionChange)="customFeeRate=null" [disabled]="flgMinConf">
|
||||
<mat-option *ngFor="let feeRateType of feeRateTypes" [value]="feeRateType.feeRateId">
|
||||
|
|
@ -49,11 +49,11 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="selFeeRate === 'customperkb' && !flgMinConf" fxFlex="48" fxLayoutAlign="end center">
|
||||
<input matInput [(ngModel)]="customFeeRate" placeholder="Fee Rate (Sats/vB)" type="number" name="custFeeRate" [step]="0.1" [min]="0" tabindex="4" #custFeeRate="ngModel" [required]="selFeeRate === 'customperkb' && !flgMinConf">
|
||||
<input matInput [(ngModel)]="customFeeRate" placeholder="Fee Rate (Sats/vByte)" type="number" name="custFeeRate" [step]="0.1" [min]="0" tabindex="4" #custFeeRate="ngModel" [required]="selFeeRate === 'customperkb' && !flgMinConf">
|
||||
<mat-error *ngIf="selFeeRate === 'customperkb' && !flgMinConf && !customFeeRate">Fee Rate is required.</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div fxFlex="48" fxLayout="row" fxLayoutAlign="start center">
|
||||
<div fxFlex="42" fxLayout="row" fxLayoutAlign="start center">
|
||||
<mat-checkbox fxFlex="2" tabindex="5" color="primary" [(ngModel)]="flgMinConf" (change)="flgMinConf ? selFeeRate=null : minConfValue=null" name="flgMinConf" fxLayoutAlign="stretch start" [ngClass]="{'mr-6': screenSize === screenSizeEnum.XS || screenSize === screenSizeEnum.SM, 'mr-2': screenSize === screenSizeEnum.MD || screenSize === screenSizeEnum.LG || screenSize === screenSizeEnum.XL}"></mat-checkbox>
|
||||
<mat-form-field fxFlex="98">
|
||||
<input matInput [(ngModel)]="minConfValue" placeholder="Min Confirmation Blocks" type="number" name="blocks" [step]="1" [min]="0" tabindex="8" #blocks="ngModel" [required]="flgMinConf" [disabled]="!flgMinConf">
|
||||
|
|
@ -62,13 +62,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<div fxLayout="column" fxLayoutAlign="space-between stretch" fxLayoutAlign.gt-sm="space-between center" fxLayout.gt-sm="row wrap" *ngIf="isCompatibleVersion">
|
||||
<mat-form-field fxFlex="48" fxLayoutAlign="start end">
|
||||
<mat-form-field fxFlex="54" fxLayoutAlign="start end">
|
||||
<mat-select tabindex="6" placeholder="Coin Selection" (selectionChange)="onUTXOSelectionChange($event)" [(value)]="selUTXOs" multiple>
|
||||
<mat-select-trigger>{{totalSelectedUTXOAmount | number}} Sats ({{selUTXOs.length > 1 ? selUTXOs.length + ' UTXOs' : '1 UTXO'}})</mat-select-trigger>
|
||||
<mat-option *ngFor="let utxo of utxos" [value]="utxo">{{utxo.value | number}} Sats</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div fxFlex="48" fxLayout="row" fxLayoutAlign="start center">
|
||||
<div fxFlex="42" fxLayout="row" fxLayoutAlign="start center">
|
||||
<mat-slide-toggle tabindex="7" color="primary" [(ngModel)]="flgUseAllBalance" (change)="onUTXOAllBalanceChange()" name="flgUseAllBalance" [disabled]="selUTXOs.length < 1">
|
||||
Use selected UTXOs balance
|
||||
</mat-slide-toggle>
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export class CLNOpenChannelComponent implements OnInit, OnDestroy {
|
|||
this.advancedTitle = this.advancedTitle + ' | Min Confirmation Blocks: ' + this.minConfValue;
|
||||
}
|
||||
if (this.selFeeRate) {
|
||||
this.advancedTitle = this.advancedTitle + ' | Fee Rate: ' + (this.customFeeRate ? (this.customFeeRate + ' (Sats/vB)') : (this.feeRateTypes.find((feeRateType) => feeRateType.feeRateId === this.selFeeRate).feeRateType));
|
||||
this.advancedTitle = this.advancedTitle + ' | Fee Rate: ' + (this.customFeeRate ? (this.customFeeRate + ' (Sats/vByte)') : (this.feeRateTypes.find((feeRateType) => feeRateType.feeRateId === this.selFeeRate).feeRateType));
|
||||
}
|
||||
if (this.selUTXOs.length && this.selUTXOs.length > 0) {
|
||||
this.advancedTitle = this.advancedTitle + ' | Total Selected: ' + this.selUTXOs.length + ' | Selected UTXOs: ' + this.decimalPipe.transform(this.totalSelectedUTXOAmount) + ' Sats';
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-sm="row wrap" fxFlex="100" fxLayoutAlign.gt-sm="space-between center">
|
||||
<div fxFlex="48" fxLayoutAlign="space-between end">
|
||||
<div fxFlex="60" fxLayoutAlign="space-between end">
|
||||
<mat-form-field [fxFlex]="channelFormGroup.controls.selFeeRate.value === 'customperkb' && !channelFormGroup.controls.flgMinConf.value ? '48' : '100'" fxLayoutAlign="start center">
|
||||
<mat-select tabindex="4" placeholder="Fee Rate" formControlName="selFeeRate">
|
||||
<mat-option *ngFor="let feeRateType of feeRateTypes" [value]="feeRateType.feeRateId">
|
||||
|
|
@ -52,11 +52,11 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="channelFormGroup.controls.selFeeRate.value === 'customperkb' && !channelFormGroup.controls.flgMinConf.value" fxFlex="48" fxLayoutAlign="end center">
|
||||
<input matInput formControlName="customFeeRate" placeholder="Fee Rate (Sats/vB)" type="number" name="custFeeRate" [step]="0.1" [min]="0" tabindex="4">
|
||||
<input matInput formControlName="customFeeRate" placeholder="Fee Rate (Sats/vByte)" type="number" name="custFeeRate" [step]="0.1" [min]="0" tabindex="4">
|
||||
<mat-error *ngIf="channelFormGroup.controls.selFeeRate.value === 'customperkb' && !channelFormGroup.controls.flgMinConf.value && !channelFormGroup.controls.customFeeRate.value">Fee Rate is required.</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div fxFlex="48" fxLayout="row" fxLayoutAlign="start center">
|
||||
<div fxFlex="35" fxLayout="row" fxLayoutAlign="start center">
|
||||
<mat-checkbox fxFlex="2" tabindex="5" color="primary" formControlName="flgMinConf" fxLayoutAlign="stretch start" [ngClass]="{'mr-6': screenSize === screenSizeEnum.XS || screenSize === screenSizeEnum.SM, 'mr-2': screenSize === screenSizeEnum.MD || screenSize === screenSizeEnum.LG || screenSize === screenSizeEnum.XL}"></mat-checkbox>
|
||||
<mat-form-field fxFlex="98">
|
||||
<input matInput formControlName="minConfValue" placeholder="Min Confirmation Blocks" type="number" name="blocks" [step]="1" [min]="0" tabindex="8" [required]="channelFormGroup.controls.flgMinConf.value">
|
||||
|
|
|
|||
|
|
@ -355,10 +355,11 @@ export class CLNEffects implements OnDestroy {
|
|||
mergeMap((action: { type: string, payload: SaveChannel }) => {
|
||||
this.store.dispatch(openSpinner({ payload: UI_MESSAGES.OPEN_CHANNEL }));
|
||||
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'SaveNewChannel', status: APICallStatusEnum.INITIATED } }));
|
||||
const newPayload = { id: action.payload.peerId, satoshis: action.payload.satoshis, feeRate: action.payload.feeRate, announce: action.payload.announce, minconf: (action.payload.minconf) ? action.payload.minconf : null };
|
||||
if (action.payload.utxos) {
|
||||
newPayload['utxos'] = action.payload.utxos;
|
||||
}
|
||||
const newPayload = { id: action.payload.peerId, satoshis: action.payload.satoshis, feeRate: action.payload.feeRate, announce: action.payload.announce };
|
||||
if (action.payload.minconf) { newPayload['minconf'] = action.payload.minconf; }
|
||||
if (action.payload.utxos) { newPayload['utxos'] = action.payload.utxos; }
|
||||
if (action.payload.requestAmount) { newPayload['request_amt'] = action.payload.requestAmount; }
|
||||
if (action.payload.compactLease) { newPayload['compact_lease'] = action.payload.compactLease; }
|
||||
return this.httpClient.post(this.CHILD_API_URL + environment.CHANNELS_API, newPayload).
|
||||
pipe(
|
||||
map((postRes: any) => {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<div fxLayout="row wrap" fxLayoutAlign="start center" fxLayoutAlign.gt-md="space-between start">
|
||||
<div fxLayout="column" fxFlex="100" fxFlex.gt-md="{{obj.width}}" *ngFor="let obj of objs; index as j;">
|
||||
<h4 fxLayoutAlign="start" class="font-bold-500">{{obj.title}}</h4>
|
||||
<span *ngIf="obj && obj.value; else emptyField">
|
||||
<span *ngIf="obj && (!!obj.value || obj.value === 0); else emptyField">
|
||||
<span [ngSwitch]="obj.type" class="foreground-secondary-text">
|
||||
<ng-container *ngSwitchCase="dataTypeEnum.ARRAY">
|
||||
<span *ngFor="let arrayObj of obj.value" class="display-block w-100" [innerHTML]="arrayObj"></span>
|
||||
|
|
|
|||
|
|
@ -60,21 +60,21 @@
|
|||
</div>
|
||||
<div fxLayout="column" fxLayout.gt-sm="row" fxFlex="100" fxLayoutAlign.gt-sm="space-between center" fxLayoutAlign="start stretch">
|
||||
<mat-form-field fxFlex="49">
|
||||
<input matInput [(ngModel)]="leaseFeeBaseSat" placeholder="Lease Base Fee (Sats)" type="number" step="1000" min="0" tabindex="3" required name="leaseFeeBaseSat">
|
||||
<input matInput [(ngModel)]="leaseFeeBaseSat" placeholder="Lease Base Fee (Sats)" type="number" step="100" min="0" tabindex="3" required name="leaseFeeBaseSat">
|
||||
<mat-error *ngIf="!leaseFeeBaseSat">Lease base fee is required.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="49">
|
||||
<input matInput [(ngModel)]="leaseFeeBasis" placeholder="Lease Base Basis (bps)" type="number" step="10" min="0" tabindex="4" required name="leaseFeeBasis">
|
||||
<input matInput [(ngModel)]="leaseFeeBasis" placeholder="Lease Base Basis (bps)" type="number" step="1" min="0" tabindex="4" required name="leaseFeeBasis">
|
||||
<mat-error *ngIf="!leaseFeeBasis">Lease base basis is required.</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div fxLayout="column" fxLayout.gt-sm="row" fxFlex="100" fxLayoutAlign.gt-sm="space-between center" fxLayoutAlign="start stretch">
|
||||
<mat-form-field fxFlex="49">
|
||||
<input matInput [(ngModel)]="channelFeeMaxBaseSat" placeholder="Max Channel Routing Base Fee (Sats)" type="number" step="1" min="0" tabindex="5" required name="channelFeeMaxBaseSat">
|
||||
<input matInput [(ngModel)]="channelFeeMaxBaseSat" placeholder="Max Channel Routing Base Fee (Sats)" type="number" step="100" min="0" tabindex="5" required name="channelFeeMaxBaseSat">
|
||||
<mat-error *ngIf="!channelFeeMaxBaseSat">Max channel routing base fee is required.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="49">
|
||||
<input matInput [(ngModel)]="channelFeeMaxProportional" placeholder="Max Channel Routing Fee Rate (ppm)" type="number" step="10" min="0" tabindex="6" required name="channelFeeMaxProportional">
|
||||
<input matInput [(ngModel)]="channelFeeMaxProportional" placeholder="Max Channel Routing Fee Rate (ppm)" type="number" step="1000" min="0" tabindex="6" required name="channelFeeMaxProportional">
|
||||
<mat-error *ngIf="!channelFeeMaxProportional">Max channel routing fee rate is required.</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export class ExperimentalSettingsComponent implements OnInit, OnDestroy {
|
|||
this.leaseFeeBaseSat = this.fundingPolicy.lease_fee_base_msat ? this.fundingPolicy.lease_fee_base_msat / 1000 : this.fundingPolicy.lease_fee_base_msat === 0 ? 0 : null;
|
||||
this.leaseFeeBasis = this.fundingPolicy.lease_fee_basis || this.fundingPolicy.lease_fee_basis === 0 ? this.fundingPolicy.lease_fee_basis : null;
|
||||
this.channelFeeMaxBaseSat = this.fundingPolicy.channel_fee_max_base_msat ? this.fundingPolicy.channel_fee_max_base_msat / 1000 : this.fundingPolicy.channel_fee_max_base_msat === 0 ? 0 : null;
|
||||
this.channelFeeMaxProportional = this.fundingPolicy.channel_fee_max_proportional_thousandths || this.fundingPolicy.channel_fee_max_proportional_thousandths === 0 ? this.fundingPolicy.channel_fee_max_proportional_thousandths : null;
|
||||
this.channelFeeMaxProportional = this.fundingPolicy.channel_fee_max_proportional_thousandths || this.fundingPolicy.channel_fee_max_proportional_thousandths === 0 ? (this.fundingPolicy.channel_fee_max_proportional_thousandths * 1000) : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ export class ExperimentalSettingsComponent implements OnInit, OnDestroy {
|
|||
onUpdateFundingPolicy() {
|
||||
this.flgUpdateCalled = false;
|
||||
this.updateMsg = '';
|
||||
this.dataService.getOrUpdateFunderPolicy(this.selPolicyType.id, this.policyMod, this.leaseFeeBaseSat, this.leaseFeeBasis, this.channelFeeMaxBaseSat * 1000, this.channelFeeMaxProportional).
|
||||
this.dataService.getOrUpdateFunderPolicy(this.selPolicyType.id, this.policyMod, this.leaseFeeBaseSat, this.leaseFeeBasis, this.channelFeeMaxBaseSat * 1000, this.channelFeeMaxProportional / 1000).
|
||||
pipe(takeUntil(this.unSubs[4])).
|
||||
subscribe({
|
||||
next: (updatePolicyRes: any) => {
|
||||
|
|
@ -141,7 +141,7 @@ export class ExperimentalSettingsComponent implements OnInit, OnDestroy {
|
|||
this.leaseFeeBaseSat = this.fundingPolicy.lease_fee_base_msat ? this.fundingPolicy.lease_fee_base_msat / 1000 : this.fundingPolicy.lease_fee_base_msat === 0 ? 0 : null;
|
||||
this.leaseFeeBasis = this.fundingPolicy.lease_fee_basis || this.fundingPolicy.lease_fee_basis === 0 ? this.fundingPolicy.lease_fee_basis : null;
|
||||
this.channelFeeMaxBaseSat = this.fundingPolicy.channel_fee_max_base_msat ? this.fundingPolicy.channel_fee_max_base_msat / 1000 : this.fundingPolicy.channel_fee_max_base_msat === 0 ? 0 : null;
|
||||
this.channelFeeMaxProportional = this.fundingPolicy.channel_fee_max_proportional_thousandths || this.fundingPolicy.channel_fee_max_proportional_thousandths === 0 ? this.fundingPolicy.channel_fee_max_proportional_thousandths : null;
|
||||
this.channelFeeMaxProportional = this.fundingPolicy.channel_fee_max_proportional_thousandths || this.fundingPolicy.channel_fee_max_proportional_thousandths === 0 ? (this.fundingPolicy.channel_fee_max_proportional_thousandths * 1000) : null;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@ export interface LookupNode {
|
|||
features?: string;
|
||||
channelCount?: number;
|
||||
nodeCapacity?: number;
|
||||
channelOpeningFee?: number;
|
||||
addresses?: Address[];
|
||||
option_will_fund?: {
|
||||
lease_fee_base_msat?: number;
|
||||
|
|
@ -402,6 +403,8 @@ export interface SaveChannel {
|
|||
feeRate?: string;
|
||||
minconf?: number;
|
||||
utxos?: string[];
|
||||
requestAmount?: string;
|
||||
compactLease?: string;
|
||||
}
|
||||
|
||||
export interface GetNewAddress {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const FEE_RATE_TYPES = [
|
|||
{ feeRateId: 'urgent', feeRateType: 'Urgent' },
|
||||
{ feeRateId: 'normal', feeRateType: 'Normal' },
|
||||
{ feeRateId: 'slow', feeRateType: 'Slow' },
|
||||
{ feeRateId: 'customperkb', feeRateType: 'Custom (Sats/vB)' }
|
||||
{ feeRateId: 'customperkb', feeRateType: 'Custom (Sats/vByte)' }
|
||||
];
|
||||
|
||||
export const NODE_SETTINGS = {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ export class mockDataService {
|
|||
return of(mockResponseData.getForwardingHistory);
|
||||
};
|
||||
|
||||
listNetworkNodes(qp: string) {
|
||||
return of(mockResponseData.getListLiqNode);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
export class mockSessionService {
|
||||
|
|
|
|||
|
|
@ -675,6 +675,54 @@ export const mockResponseData = {
|
|||
],
|
||||
last_offset_index: 2
|
||||
},
|
||||
getListLiqNode: [
|
||||
{
|
||||
nodeid: '0263983b2261d6ad9e2e134c1aa60d0c82d47c1c16b876a096a38d4f283fc236dc',
|
||||
alias: 'blyte c-lightning',
|
||||
color: '026398',
|
||||
last_timestamp: 1640062643,
|
||||
features: '800088282a6aa2',
|
||||
addresses: [
|
||||
{
|
||||
type: 'ipv4',
|
||||
address: '172.105.98.46',
|
||||
port: 9747
|
||||
}
|
||||
],
|
||||
option_will_fund: {
|
||||
lease_fee_base_msat: '20000000',
|
||||
lease_fee_basis: 50,
|
||||
funding_weight: 666,
|
||||
channel_fee_max_base_msat: '0',
|
||||
channel_fee_max_proportional_thousandths: 1,
|
||||
compact_lease: '029a0032000100004e20'
|
||||
},
|
||||
channelOpeningFee: 22165
|
||||
},
|
||||
{
|
||||
nodeid: '023b485342839753dea66ff280df74e73570abc8160ad9a3456267dc5249e7a749',
|
||||
alias: 'clight-test2',
|
||||
color: '023b48',
|
||||
last_timestamp: 1636307430,
|
||||
features: '800088282a6aa2',
|
||||
addresses: [
|
||||
{
|
||||
type: 'torv3',
|
||||
address: 'cll5xqmdxfk6xn73x6ic423qleii3fcfb63vvbkhxwrmnhiyh2cb5xqd.onion',
|
||||
port: 9735
|
||||
}
|
||||
],
|
||||
option_will_fund: {
|
||||
lease_fee_base_msat: '10000000',
|
||||
lease_fee_basis: 50,
|
||||
funding_weight: 666,
|
||||
channel_fee_max_base_msat: '1000',
|
||||
channel_fee_max_proportional_thousandths: 1,
|
||||
compact_lease: '029a003200010000271003e8'
|
||||
},
|
||||
channelOpeningFee: 12165
|
||||
}
|
||||
],
|
||||
setSelectedNodeSuccess: {
|
||||
status: 200,
|
||||
body: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue