Release 0.15.3 (#1467)

* Fix `Unknown command` error when disabling offers on CLN. ([#1443]) (#1451)
* Add missing SSO options to config (#1455)
* Fix for cln logic screen navigation (#1457)
* Transactions destination address display fix (#1458)
* cln delexpiredinvoices deprecation fix (#1459)
* Read LN_IMPLEMENTATION from environment (#1460)
* Add Fee Rate Information on Send Funds Modal (#1461)
* Artifact script fix (#1464)
* Add AMP toggle for LND Send Payments (#1466)

---------

Co-authored-by: Se7enZ <118189041+s373nZ@users.noreply.github.com>
This commit is contained in:
ShahanaFarooqui 2024-11-10 20:23:52 -08:00 committed by GitHub
parent 6027167e2a
commit a594606d27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 4011 additions and 4047 deletions

View File

@ -116,7 +116,6 @@
"no-label-var": "error",
"no-restricted-globals": "error",
"no-undef-init": "error",
"no-undefined": "error",
"block-spacing": "error",
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"comma-style": "error",

View File

@ -2,31 +2,29 @@ name: Artifact
on:
push:
branches: [ master, 'Release-*' ]
tags: [ 'v*' ]
release:
types: [released]
# Triggers the workflow only when merging pull request to the branches.
pull_request:
types: [closed]
branches: [ master, 'Release-*' ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
version:
description: 'Release version'
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Cache node_modules
uses: actions/cache@v2
uses: actions/cache@v4
id: cache-npm-packages
with:
path: node_modules
@ -37,7 +35,7 @@ jobs:
run: npm ci --legacy-peer-deps
- name: Cache build frontend
uses: actions/cache@v2
uses: actions/cache@v4
id: cache-build-frontend
with:
path: frontend
@ -47,7 +45,7 @@ jobs:
run: npm run buildfrontend
- name: Cache build backend
uses: actions/cache@v2
uses: actions/cache@v4
id: cache-build-backend
with:
path: backend
@ -61,26 +59,32 @@ jobs:
needs: build
steps:
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Cache build frontend
uses: actions/cache@v2
uses: actions/cache@v4
id: cache-build-frontend
with:
path: frontend
key: ${{ runner.os }}-frontend-${{ github.sha }}
- name: Cache build backend
uses: actions/cache@v2
uses: actions/cache@v4
id: cache-build-backend
with:
path: backend
key: ${{ runner.os }}-backend-${{ github.sha }}
- name: Compress files
run: tar -czf /tmp/rtlbuild.tar.gz frontend backend rtl.js package.json package-lock.json
- uses: actions/upload-artifact@v2
env:
VERSION: "${{ github.event.release.tag_name || github.event.inputs.version || '' }}"
run: |
tar -czf /tmp/rtl-build-$VERSION.tar.gz frontend backend rtl.js package.json package-lock.json
zip -r /tmp/rtl-build-$VERSION.zip frontend backend rtl.js package.json package-lock.json
- uses: actions/upload-artifact@v3
with:
name: rtl-build-${{ github.event.release.tag_name }}
path: /tmp/rtlbuild.tar.gz
name: rtl-build-${{ github.event.release.tag_name || github.event.inputs.version || '' }}
path: |
/tmp/rtl-build-${{ github.event.release.tag_name || github.event.inputs.version || '' }}.tar.gz
/tmp/rtl-build-${{ github.event.release.tag_name || github.event.inputs.version || '' }}.zip

View File

@ -10,11 +10,11 @@ export const deleteExpiredInvoice = (req, res, next) => {
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/delexpiredinvoice';
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/autoclean-once';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoices Deleted', data: body });
res.status(204).json({ status: 'Invoice Deleted Successfully' });
res.status(201).json({ status: 'Cleaned Invoices: ' + body.autoclean.expiredinvoices.cleaned + ', Uncleaned Invoices: ' + body.autoclean.expiredinvoices.uncleaned });
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'Delete Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });

View File

@ -83,7 +83,7 @@ export const disableOffer = (req, res, next) => {
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/disableOffer';
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/disableoffer';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Disabled', data: body });

View File

@ -28,7 +28,7 @@ export const simplifyAllChannels = (selNode, channels) => {
});
channelNodeIds = channelNodeIds.substring(1);
options.url = selNode.settings.lnServerUrl + '/nodes';
options.form = { nodeIds: channelNodeIds };
options.form = channelNodeIds;
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Node Ids to find alias', data: channelNodeIds });
return request.post(options).then((nodes) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Filtered Nodes Received', data: nodes });

View File

@ -155,47 +155,6 @@ export const postChannel = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const postTransactions = (req, res, next) => {
const { paymentReq, paymentAmount, feeLimit, outgoingChannel, allowSelfPayment, lastHopPubkey } = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sending Payment..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/channels/transaction-stream';
options.form = { payment_request: paymentReq };
if (paymentAmount) {
options.form.amt = paymentAmount;
}
if (feeLimit) {
options.form.fee_limit = feeLimit;
}
if (outgoingChannel) {
options.form.outgoing_chan_id = outgoingChannel;
}
if (allowSelfPayment) {
options.form.allow_self_payment = allowSelfPayment;
}
if (lastHopPubkey) {
options.form.last_hop_pubkey = Buffer.from(lastHopPubkey, 'hex').toString('base64');
}
options.form = JSON.stringify(options.form);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form });
request.post(options).then((body) => {
body = body.result ? body.result : body;
if (body.payment_error) {
const err = common.handleError(body.payment_error, 'Channels', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
else {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Payment Sent', data: body });
res.status(201).json(body);
}
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const closeChannel = (req, res, next) => {
try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' });

View File

@ -97,3 +97,33 @@ export const paymentLookup = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const sendPayment = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sending Payment..' });
options = common.getOptions(req);
if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: options.error });
}
options.url = req.session.selectedNode.settings.lnServerUrl + '/v2/router/send';
if (req.body.last_hop_pubkey) {
req.body.last_hop_pubkey = Buffer.from(req.body.last_hop_pubkey, 'hex').toString('base64');
}
req.body.amp = req.body.amp ?? false;
req.body.timeout_seconds = req.body.timeout_seconds || 600;
options.form = JSON.stringify(req.body);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Send Payment Options', data: options.form });
request.post(options).then((body) => {
const results = body.split('\n').filter(Boolean).map((jsonString) => JSON.parse(jsonString));
body = results.length > 0 ? results[results.length - 1] : { result: { status: 'UNKNOWN' } };
if (body.result.status === 'FAILED') {
const err = common.handleError(common.titleCase(body.result.failure_reason.replace(/_/g, ' ').replace('FAILURE REASON ', '')), 'Payments', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
if (body.result.status === 'SUCCEEDED') {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent', data: body.result });
res.status(201).json(body.result);
}
}).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

View File

@ -1,13 +1,12 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getAllChannels, getPendingChannels, getClosedChannels, postChannel, postTransactions, closeChannel, postChanPolicy } from '../../controllers/lnd/channels.js';
import { getAllChannels, getPendingChannels, getClosedChannels, postChannel, closeChannel, postChanPolicy } from '../../controllers/lnd/channels.js';
const router = Router();
router.get('/', isAuthenticated, getAllChannels);
router.get('/pending', isAuthenticated, getPendingChannels);
router.get('/closed', isAuthenticated, getClosedChannels);
router.post('/', isAuthenticated, postChannel);
router.post('/transactions', isAuthenticated, postTransactions);
router.delete('/:channelPoint', isAuthenticated, closeChannel);
router.post('/chanPolicy', isAuthenticated, postChanPolicy);
export default router;

View File

@ -1,11 +1,12 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { decodePayment, decodePayments, getPayments, getAllLightningTransactions, paymentLookup } from '../../controllers/lnd/payments.js';
import { decodePayment, decodePayments, getPayments, getAllLightningTransactions, paymentLookup, sendPayment } from '../../controllers/lnd/payments.js';
const router = Router();
router.get('/', isAuthenticated, getPayments);
router.get('/alltransactions', isAuthenticated, getAllLightningTransactions);
router.get('/decode/:payRequest', isAuthenticated, decodePayment);
router.get('/lookup/:paymentHash', isAuthenticated, paymentLookup);
router.post('/', isAuthenticated, decodePayments);
router.post('/send', isAuthenticated, sendPayment);
export default router;

View File

@ -395,7 +395,8 @@ export class CommonService {
}
});
};
this.readCookie = () => {
this.readCookie = (config) => {
this.appConfig.SSO = config.SSO;
const exists = fs.existsSync(this.appConfig.SSO.rtlCookiePath);
if (exists) {
try {

View File

@ -146,7 +146,7 @@ export class ConfigService {
this.common.nodes[idx] = { settings: { blockExplorerUrl: '' }, authentication: {} };
this.common.nodes[idx].index = node.index;
this.common.nodes[idx].lnNode = node.lnNode;
this.common.nodes[idx].lnImplementation = (process?.env?.lnImplementation) ? process?.env?.lnImplementation : node.lnImplementation ? node.lnImplementation : 'LND';
this.common.nodes[idx].lnImplementation = (process?.env?.LN_IMPLEMENTATION) ? process?.env?.LN_IMPLEMENTATION : node.lnImplementation ? node.lnImplementation : 'LND';
if (this.common.nodes[idx].lnImplementation === 'CLT') {
this.common.nodes[idx].lnImplementation = 'CLN';
}
@ -343,7 +343,7 @@ export class ConfigService {
this.errMsg = 'Please set rtlCookiePath value for single sign on option!';
}
else {
this.common.readCookie();
this.common.readCookie(config);
}
}
};

View File

@ -74,6 +74,7 @@ docker build -t rtl:${RTL_VERSION} -f dockerfiles/Dockerfile .
Create an environment file with your required configurations. Sample .env:
```
RTL_CONFIG_PATH=/RTLConfig
LN_IMPLEMENTATION=LND
MACAROON_PATH=/LNDMacaroon
LN_SERVER_URL=https://host.docker.internal:8080

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

@ -1,5 +1,27 @@
@angular/animations
MIT
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/cdk
MIT
@ -28,15 +50,81 @@ THE SOFTWARE.
@angular/common
MIT
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/core
MIT
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/flex-layout
MIT
@angular/forms
MIT
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/material
MIT
@ -65,9 +153,53 @@ THE SOFTWARE.
@angular/platform-browser
MIT
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@angular/router
MIT
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@babel/runtime
MIT
@ -1565,9 +1697,6 @@ THE SOFTWARE.
elliptic
MIT
encode-utf8
MIT
events
MIT
MIT

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

@ -1 +0,0 @@
(()=>{"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+"."+{17:"d00d31d08d7bad32",190:"88ca997666a3998a",193:"b1206fbf24aa1327",853:"c4ce8a9a0bef2bb7"}[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={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,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))})()})();

View File

@ -0,0 +1 @@
(()=>{"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,d)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,d]=e[n],c=!0,o=0;o<t.length;o++)(!1&d||a>=d)&&Object.keys(r.O).every(b=>r.O[b](t[o]))?t.splice(o--,1):(c=!1,d<a&&(a=d));if(c){e.splice(n--,1);var u=i();void 0!==u&&(f=u)}}return f}d=d||0;for(var n=e.length;n>0&&e[n-1][2]>d;n--)e[n]=e[n-1];e[n]=[t,i,d]},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:"6fa7154eb6e447e2",190:"03f035c34a56c8be",193:"0e1a81316bbc29da",853:"50b06a24091d386f"}[e]+".js",r.miniCssF=e=>{},r.o=(e,f)=>Object.prototype.hasOwnProperty.call(e,f),(()=>{var e={},f="RTLApp:";r.l=(t,i,d,n)=>{if(e[t])e[t].push(i);else{var a,c;if(void 0!==d)for(var o=document.getElementsByTagName("script"),u=0;u<o.length;u++){var l=o[u];if(l.getAttribute("src")==t||l.getAttribute("data-webpack")==f+d){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",f+d),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: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,d)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)d.push(n[2]);else if(121!=i){var a=new Promise((l,s)=>n=e[i]=[l,s]);d.push(n[2]=a);var c=r.p+r.u(i),o=new Error;r.l(c,l=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var s=l&&("load"===l.type?"missing":l.type),p=l&&l.target&&l.target.src;o.message="Loading chunk "+i+" failed.\n("+s+": "+p+")",o.name="ChunkLoadError",o.type=s,o.request=p,n[1](o)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var f=(i,d)=>{var o,u,[n,a,c]=d,l=0;if(n.some(p=>0!==e[p])){for(o in a)r.o(a,o)&&(r.m[o]=a[o]);if(c)var s=c(r)}for(i&&i(d);l<n.length;l++)r.o(e,u=n[l])&&e[u]&&e[u][0](),e[u]=0;return r.O(s)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(f.bind(null,0)),t.push=f.bind(null,t.push.bind(t))})()})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

7371
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "rtl",
"version": "0.15.2-beta",
"version": "0.15.3-beta",
"license": "MIT",
"type": "module",
"scripts": {

View File

@ -9,11 +9,11 @@ export const deleteExpiredInvoice = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Deleting Expired Invoices..' });
options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/delexpiredinvoice';
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/autoclean-once';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoices Deleted', data: body });
res.status(204).json({ status: 'Invoice Deleted Successfully' });
res.status(201).json({ status: 'Cleaned Invoices: ' + body.autoclean.expiredinvoices.cleaned + ', Uncleaned Invoices: ' + body.autoclean.expiredinvoices.uncleaned });
}).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'Delete Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });

View File

@ -82,7 +82,7 @@ export const disableOffer = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Disabling Offer..' });
options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/disableOffer';
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/disableoffer';
options.body = req.body;
request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Disabled', data: body });

View File

@ -31,7 +31,7 @@ export const simplifyAllChannels = (selNode: SelectedNode, channels) => {
});
channelNodeIds = channelNodeIds.substring(1);
options.url = selNode.settings.lnServerUrl + '/nodes';
options.form = { nodeIds: channelNodeIds };
options.form = channelNodeIds;
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Node Ids to find alias', data: channelNodeIds });
return request.post(options).then((nodes) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Filtered Nodes Received', data: nodes });

View File

@ -155,37 +155,6 @@ export const postChannel = (req, res, next) => {
});
};
export const postTransactions = (req, res, next) => {
const { paymentReq, paymentAmount, feeLimit, outgoingChannel, allowSelfPayment, lastHopPubkey } = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sending Payment..' });
options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/channels/transaction-stream';
options.form = { payment_request: paymentReq };
if (paymentAmount) {
options.form.amt = paymentAmount;
}
if (feeLimit) { options.form.fee_limit = feeLimit; }
if (outgoingChannel) { options.form.outgoing_chan_id = outgoingChannel; }
if (allowSelfPayment) { options.form.allow_self_payment = allowSelfPayment; }
if (lastHopPubkey) { options.form.last_hop_pubkey = Buffer.from(lastHopPubkey, 'hex').toString('base64'); }
options.form = JSON.stringify(options.form);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Send Payment Options', data: options.form });
request.post(options).then((body) => {
body = body.result ? body.result : body;
if (body.payment_error) {
const err = common.handleError(body.payment_error, 'Channels', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
} else {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Payment Sent', data: body });
res.status(201).json(body);
}
}).catch((errRes) => {
const err = common.handleError(errRes, 'Channels', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const closeChannel = (req, res, next) => {
try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' });

View File

@ -96,3 +96,32 @@ export const paymentLookup = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};
export const sendPayment = (req, res, next) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Sending Payment..' });
options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
options.url = req.session.selectedNode.settings.lnServerUrl + '/v2/router/send';
if (req.body.last_hop_pubkey) {
req.body.last_hop_pubkey = Buffer.from(req.body.last_hop_pubkey, 'hex').toString('base64');
}
req.body.amp = req.body.amp ?? false;
req.body.timeout_seconds = req.body.timeout_seconds || 600;
options.form = JSON.stringify(req.body);
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Payments', msg: 'Send Payment Options', data: options.form });
request.post(options).then((body) => {
const results = body.split('\n').filter(Boolean).map((jsonString) => JSON.parse(jsonString));
body = results.length > 0 ? results[results.length - 1] : { result: { status: 'UNKNOWN' } };
if (body.result.status === 'FAILED') {
const err = common.handleError(common.titleCase(body.result.failure_reason.replace(/_/g, ' ').replace('FAILURE REASON ', '')), 'Payments', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
if (body.result.status === 'SUCCEEDED') {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Payments', msg: 'Payment Sent', data: body.result });
res.status(201).json(body.result);
}
}).catch((errRes) => {
const err = common.handleError(errRes, 'Payments', 'Send Payment Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
});
};

View File

@ -1,7 +1,7 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { getAllChannels, getPendingChannels, getClosedChannels, postChannel, postTransactions, closeChannel, postChanPolicy } from '../../controllers/lnd/channels.js';
import { getAllChannels, getPendingChannels, getClosedChannels, postChannel, closeChannel, postChanPolicy } from '../../controllers/lnd/channels.js';
const router = Router();
@ -9,7 +9,6 @@ router.get('/', isAuthenticated, getAllChannels);
router.get('/pending', isAuthenticated, getPendingChannels);
router.get('/closed', isAuthenticated, getClosedChannels);
router.post('/', isAuthenticated, postChannel);
router.post('/transactions', isAuthenticated, postTransactions);
router.delete('/:channelPoint', isAuthenticated, closeChannel);
router.post('/chanPolicy', isAuthenticated, postChanPolicy);

View File

@ -1,7 +1,7 @@
import exprs from 'express';
const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js';
import { decodePayment, decodePayments, getPayments, getAllLightningTransactions, paymentLookup } from '../../controllers/lnd/payments.js';
import { decodePayment, decodePayments, getPayments, getAllLightningTransactions, paymentLookup, sendPayment } from '../../controllers/lnd/payments.js';
const router = Router();
@ -10,5 +10,6 @@ router.get('/alltransactions', isAuthenticated, getAllLightningTransactions);
router.get('/decode/:payRequest', isAuthenticated, decodePayment);
router.get('/lookup/:paymentHash', isAuthenticated, paymentLookup);
router.post('/', isAuthenticated, decodePayments);
router.post('/send', isAuthenticated, sendPayment);
export default router;

View File

@ -413,7 +413,8 @@ export class CommonService {
});
};
public readCookie = () => {
public readCookie = (config) => {
this.appConfig.SSO = config.SSO;
const exists = fs.existsSync(this.appConfig.SSO.rtlCookiePath);
if (exists) {
try {

View File

@ -150,7 +150,7 @@ export class ConfigService {
this.common.nodes[idx] = { settings: { blockExplorerUrl: '' }, authentication: {} };
this.common.nodes[idx].index = node.index;
this.common.nodes[idx].lnNode = node.lnNode;
this.common.nodes[idx].lnImplementation = (process?.env?.lnImplementation) ? process?.env?.lnImplementation : node.lnImplementation ? node.lnImplementation : 'LND';
this.common.nodes[idx].lnImplementation = (process?.env?.LN_IMPLEMENTATION) ? process?.env?.LN_IMPLEMENTATION : node.lnImplementation ? node.lnImplementation : 'LND';
if (this.common.nodes[idx].lnImplementation === 'CLT') { this.common.nodes[idx].lnImplementation = 'CLN'; }
switch (this.common.nodes[idx].lnImplementation) {
case 'CLN':
@ -321,7 +321,7 @@ export class ConfigService {
if (!config.SSO.rtlCookiePath || config.SSO.rtlCookiePath.trim() === '') {
this.errMsg = 'Please set rtlCookiePath value for single sign on option!';
} else {
this.common.readCookie();
this.common.readCookie(config);
}
}
};

View File

@ -6,7 +6,20 @@
</div>
<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-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()">
<mat-form-field fxLayout="column" fxFlex="55">
<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 { MatSnackBar } from '@angular/material/snack-bar';
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 { Node, RTLConfiguration } from '../../../shared/models/RTLconfig';
@ -25,6 +25,8 @@ import { setChannelTransaction } from '../../store/cln.actions';
import { rootAppConfig, rootSelectedNode } from '../../../store/rtl.selector';
import { clnNodeInformation, utxoBalances } from '../../store/cln.selector';
import { ApiCallStatusPayload } from '../../../shared/models/apiCallsPayload';
import { DataService } from 'src/app/shared/services/data.service';
import { RecommendedFeeRates } from 'src/app/shared/models/rtlModels';
@Component({
selector: 'rtl-cln-on-chain-send-modal',
@ -37,6 +39,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
@ViewChild('formSweepAll', { static: false }) formSweepAll: any;
@ViewChild('stepper', { static: false }) stepper: MatStepper;
public faExclamationTriangle = faExclamationTriangle;
public faInfoCircle = faInfoCircle;
public sweepAll = false;
public selNode: Node | null;
public appConfig: RTLConfiguration;
@ -65,6 +68,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
public advancedTitle = 'Advanced Options';
public flgValidated = false;
public flgEditable = true;
public recommendedFee: RecommendedFeeRates = { fastestFee: 0, halfHourFee: 0, hourFee: 0 };
public passwordFormLabel = 'Authenticate with your RTL password';
public sendFundFormLabel = 'Sweep funds';
public confirmFormLabel = 'Confirm sweep';
@ -74,12 +78,13 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
confirmFormGroup: UntypedFormGroup;
public screenSize = '';
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(
public dialogRef: MatDialogRef<CLNOnChainSendModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: CLNOnChainSendFunds,
private logger: LoggerService,
private dataService: DataService,
private store: Store<RTLState>,
private commonService: CommonService,
private decimalPipe: DecimalPipe,
@ -91,6 +96,13 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
}
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.passwordFormGroup = this.formBuilder.group({
hiddenPassword: ['', [Validators.required]],
@ -104,7 +116,7 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
minConfValue: [{ value: null, disabled: true }]
});
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) {
this.sendFundFormGroup.controls.selFeeRate.disable();
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.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.reset();
if (feeRate === 'customperkb' && !this.sendFundFormGroup.controls.flgMinConf.value) {
@ -130,23 +142,23 @@ export class CLNOnChainSendModalComponent implements OnInit, OnDestroy {
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]) => {
this.fiatConversion = selNode.settings.fiatConversion;
this.amountUnits = selNode.settings.currencyUnits;
this.appConfig = appConfig;
});
this.store.select(clnNodeInformation).pipe(takeUntil(this.unSubs[2])).
this.store.select(clnNodeInformation).pipe(takeUntil(this.unSubs[4])).
subscribe((nodeInfo: GetInfo) => {
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 }) => {
this.utxos = this.commonService.sortAscByKey(utxoBalancesSeletor.utxos?.filter((utxo) => utxo.status === 'confirmed'), 'value');
this.logger.info(utxoBalancesSeletor);
});
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)).
subscribe((action: any) => {
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) {
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({
next: (data) => {
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;
if (this.transaction.satoshi && this.selAmountUnit !== event.value) {
this.commonService.convertCurrency(+this.transaction.satoshi, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[6])).
pipe(takeUntil(this.unSubs[8])).
subscribe({
next: (data) => {
this.selAmountUnit = event.value;

View File

@ -58,7 +58,7 @@
<div fxFlex="64" fxLayout="row" fxLayoutAlign="space-between center">
<mat-form-field fxLayout="column" fxLayoutAlign="start center" [fxFlex]="selFeeRate === 'customperkb' && !flgMinConf ? '40' : '100'">
<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">
{{feeRateType.feeRateType}}
</mat-option>

View File

@ -168,6 +168,13 @@ export class CLNOpenChannelComponent implements OnInit, OnDestroy {
}
} else {
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 }));
}
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() {
this.unSubs.forEach((completeSub) => {
completeSub.next(<any>null);

View File

@ -14,7 +14,7 @@ import { WebSocketClientService } from '../../shared/services/web-socket.service
import { ErrorMessageComponent } from '../../shared/components/data-modal/error-message/error-message.component';
import { CLNInvoiceInformationComponent } from '../transactions/invoices/invoice-information-modal/invoice-information.component';
import { GetInfo, Payment, FeeRates, ListInvoices, Invoice, Peer, OnChain, QueryRoutes, SaveChannel, GetNewAddress, DetachPeer, UpdateChannel, CloseChannel, SendPayment, GetQueryRoutes, ChannelLookup, Channel, OfferInvoice, Offer } from '../../shared/models/clnModels';
import { API_URL, API_END_POINTS, AlertTypeEnum, APICallStatusEnum, UI_MESSAGES, CLNWSEventTypeEnum, CLNActions, RTLActions, CLNForwardingEventsStatusEnum } from '../../shared/services/consts-enums-functions';
import { API_URL, API_END_POINTS, SECS_IN_YEAR, AlertTypeEnum, APICallStatusEnum, UI_MESSAGES, CLNWSEventTypeEnum, CLNActions, RTLActions, CLNForwardingEventsStatusEnum } from '../../shared/services/consts-enums-functions';
import { closeAllDialogs, closeSpinner, logout, openAlert, openSnackBar, openSpinner, setApiUrl, setNodeData } from '../../store/rtl.actions';
import { RTLState } from '../../store/rtl.state';
@ -114,8 +114,8 @@ export class CLNEffects implements OnDestroy {
}),
catchError((err) => {
const code = this.commonService.extractErrorCode(err);
const msg = (code === 'ETIMEDOUT') ? 'Unable to Connect to Core Lightning Server.' : this.commonService.extractErrorMessage(err);
this.router.navigate(['/login'], { state: { logoutReason: JSON.stringify(msg) } });
const msg = (code === 503) ? 'Unable to Connect to Core Lightning Server.' : this.commonService.extractErrorMessage(err);
this.router.navigate(['/error'], { state: { errorCode: code, errorMessage: msg } });
this.handleErrorWithoutAlert('FetchInfo', UI_MESSAGES.GET_NODE_INFO, 'Fetching Node Info Failed.', { status: code, error: msg });
return of({ type: RTLActions.VOID });
})
@ -608,12 +608,12 @@ export class CLNEffects implements OnDestroy {
ofType(CLNActions.DELETE_EXPIRED_INVOICE_CLN),
mergeMap((action: { type: string, payload: number }) => {
this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DELETE_INVOICE }));
return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.INVOICES_API + '/delete', { maxexpiry: action.payload }).
return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.INVOICES_API + '/delete', { subsystem: 'expiredinvoices', age: SECS_IN_YEAR }).
pipe(
map((postRes: any) => {
this.logger.info(postRes);
this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DELETE_INVOICE }));
this.store.dispatch(openSnackBar({ payload: 'Invoices Deleted Successfully!' }));
this.store.dispatch(openSnackBar({ payload: postRes.status }));
return { type: CLNActions.FETCH_INVOICES_CLN };
}),
catchError((err: any) => {

View File

@ -6,7 +6,20 @@
</div>
<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-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()">
<mat-form-field fxLayout="column" fxFlex="55">
<mat-label>Bitcoin Address</mat-label>

View File

@ -5,7 +5,7 @@ import { takeUntil, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
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 { GetInfo, OnChainBalance, SendPaymentOnChain } from '../../../shared/models/eclModels';
@ -17,6 +17,8 @@ import { RTLState } from '../../../store/rtl.state';
import { openSnackBar } from '../../../store/rtl.actions';
import { sendOnchainFunds } from '../../store/ecl.actions';
import { rootSelectedNode } from '../../../store/rtl.selector';
import { DataService } from 'src/app/shared/services/data.service';
import { RecommendedFeeRates } from 'src/app/shared/models/rtlModels';
@Component({
selector: 'rtl-ecl-on-chain-send-modal',
@ -27,6 +29,7 @@ export class ECLOnChainSendModalComponent implements OnInit, OnDestroy {
@ViewChild('form', { static: true }) form: any;
public faExclamationTriangle = faExclamationTriangle;
public faInfoCircle = faInfoCircle;
public selNode: Node | null;
public addressTypes = [];
public selectedAddress = ADDRESS_TYPES[1];
@ -41,19 +44,27 @@ export class ECLOnChainSendModalComponent implements OnInit, OnDestroy {
public currConvertorRate = {};
public unitConversionValue = 0;
public currencyUnitFormats = CURRENCY_UNIT_FORMATS;
public recommendedFee: RecommendedFeeRates = { fastestFee: 0, halfHourFee: 0, hourFee: 0 };
public amountError = 'Amount is Required.';
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() {
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.amountUnits = selNode.settings.currencyUnits;
this.logger.info(selNode);
});
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)
).
subscribe((action: any) => {
@ -74,7 +85,7 @@ export class ECLOnChainSendModalComponent implements OnInit, OnDestroy {
this.sendFundError = '';
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).
pipe(takeUntil(this.unSubs[2])).
pipe(takeUntil(this.unSubs[3])).
subscribe({
next: (data) => {
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;
if (this.transaction.amount && this.selAmountUnit !== event.value) {
this.commonService.convertCurrency(this.transaction.amount, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[3])).
pipe(takeUntil(this.unSubs[4])).
subscribe({
next: (data) => {
this.selAmountUnit = event.value;

View File

@ -6,8 +6,21 @@
</div>
<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-content class="padding-gap-x-large">
<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-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()">
<mat-form-field fxLayout="column" fxFlex.gt-sm="55">
<mat-label>Bitcoin Address</mat-label>
<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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
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 { Node, RTLConfiguration } from '../../../shared/models/RTLconfig';
@ -23,6 +23,8 @@ import { RTLState } from '../../../store/rtl.state';
import { isAuthorized, openSnackBar } from '../../../store/rtl.actions';
import { setChannelTransaction } from '../../store/lnd.actions';
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({
selector: 'rtl-on-chain-send-modal',
@ -35,6 +37,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
@ViewChild('formSweepAll', { static: false }) formSweepAll: any;
@ViewChild('stepper', { static: false }) stepper: MatStepper;
public faExclamationTriangle = faExclamationTriangle;
public faInfoCircle = faInfoCircle;
public sweepAll = false;
public selNode: Node | null;
public appConfig: RTLConfiguration;
@ -58,6 +61,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
public sendFundError = '';
public flgValidated = false;
public flgEditable = true;
public recommendedFee: RecommendedFeeRates = { fastestFee: 0, halfHourFee: 0, hourFee: 0 };
public passwordFormLabel = 'Authenticate with your RTL password';
public sendFundFormLabel = 'Sweep funds';
public confirmFormLabel = 'Confirm sweep';
@ -65,12 +69,13 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
passwordFormGroup: UntypedFormGroup;
sendFundFormGroup: 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(
public dialogRef: MatDialogRef<OnChainSendModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: OnChainSendFunds,
private logger: LoggerService,
private dataService: DataService,
private store: Store<RTLState>,
private rtlEffects: RTLEffects,
private commonService: CommonService,
@ -80,6 +85,13 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
private formBuilder: UntypedFormBuilder) { }
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.passwordFormGroup = this.formBuilder.group({
hiddenPassword: ['', [Validators.required]],
@ -92,7 +104,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
selTransType: ['1', Validators.required]
});
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') {
this.sendFundFormGroup.controls.transactionBlocks.setValidators([Validators.required]);
this.sendFundFormGroup.controls.transactionBlocks.setValue(null);
@ -105,16 +117,16 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
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.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.amountUnits = selNode.settings.currencyUnits;
this.logger.info(selNode);
});
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)).
subscribe((action: any) => {
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) {
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({
next: (data) => {
this.selAmountUnit = CurrencyUnitEnum.SATS;
@ -254,7 +266,7 @@ export class OnChainSendModalComponent implements OnInit, OnDestroy {
if (this.transactionAmount && this.selAmountUnit !== event.value) {
const amount = this.transactionAmount ? this.transactionAmount : 0;
this.commonService.convertCurrency(amount, prevSelectedUnit, currSelectedUnit, this.amountUnits[2], this.fiatConversion).
pipe(takeUntil(this.unSubs[5])).
pipe(takeUntil(this.unSubs[6])).
subscribe({
next: (data) => {
this.selAmountUnit = event.value;

View File

@ -89,7 +89,7 @@
<mat-option (click)="onUTXOClick(utxo)">View Info</mat-option>
<mat-option (click)="onLabelUTXO(utxo)">Label</mat-option>
<mat-option (click)="onLeaseUTXO(utxo)">Lease</mat-option>
<mat-option *ngIf="!utxo.label.toLowerCase().includes('sweep') && utxo.confirmations === '0'" (click)="onBumpFee(utxo)">Bump Fee</mat-option>
<mat-option *ngIf="utxo.label && !utxo.label.toLowerCase().includes('sweep') && utxo.confirmations === '0'" (click)="onBumpFee(utxo)">Bump Fee</mat-option>
</mat-select>
</div>
</td>

View File

@ -12,8 +12,8 @@ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { ChannelRebalanceAlert } from '../../../../shared/models/alertData';
import { LoggerService } from '../../../../shared/services/logger.service';
import { CommonService } from '../../../../shared/services/common.service';
import { Channel, QueryRoutes, ListInvoices } from '../../../../shared/models/lndModels';
import { DEFAULT_INVOICE_EXPIRY, FEE_LIMIT_TYPES, LNDActions, PAGE_SIZE, ScreenSizeEnum, UI_MESSAGES } from '../../../../shared/services/consts-enums-functions';
import { Channel, QueryRoutes, ListInvoices, SendPayment } from '../../../../shared/models/lndModels';
import { DEFAULT_INVOICE_EXPIRY, FEE_LIMIT_TYPES, LNDActions, PAGE_SIZE, ScreenSizeEnum, UI_MESSAGES, getFeeLimitSat } from '../../../../shared/services/consts-enums-functions';
import { RTLState } from '../../../../store/rtl.state';
import { saveNewInvoice, sendPayment } from '../../../store/lnd.actions';
@ -239,13 +239,29 @@ export class ChannelRebalanceComponent implements OnInit, OnDestroy {
this.flgInvoiceGenerated = true;
this.paymentRequest = payReq;
if (this.feeFormGroup.controls.selFeeLimitType.value.id === 'percent' && !(+this.feeFormGroup.controls.feeLimit.value % 1 === 0)) {
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.NO_SPINNER, paymentReq: payReq, outgoingChannel: this.selChannel,
feeLimitType: 'fixed', feeLimit: Math.ceil((+this.feeFormGroup.controls.feeLimit.value * +this.inputFormGroup.controls.rebalanceAmount.value) / 100),
allowSelfPayment: true, lastHopPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey, fromDialog: true } }));
const payload: SendPayment = {
uiMessage: UI_MESSAGES.NO_SPINNER,
payment_request: payReq,
amp: false,
outgoing_chan_ids: this.selChannel?.chan_id ? [this.selChannel?.chan_id] : undefined,
fee_limit_sat: Math.ceil(getFeeLimitSat('fixed', this.feeFormGroup.controls.feeLimit.value, (this.inputFormGroup.controls.rebalanceAmount.value || 0))),
allow_self_payment: true,
last_hop_pubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey,
fromDialog: true
};
this.store.dispatch(sendPayment({ payload }));
} else {
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.NO_SPINNER, paymentReq: payReq, outgoingChannel: this.selChannel,
feeLimitType: this.feeFormGroup.controls.selFeeLimitType.value.id, feeLimit: this.feeFormGroup.controls.feeLimit.value, allowSelfPayment: true,
lastHopPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey, fromDialog: true } }));
const payload: SendPayment = {
uiMessage: UI_MESSAGES.NO_SPINNER,
payment_request: payReq,
amp: false,
outgoing_chan_ids: this.selChannel?.chan_id ? [this.selChannel?.chan_id] : undefined,
fee_limit_sat: getFeeLimitSat(this.feeFormGroup.controls.selFeeLimitType.value.id, this.feeFormGroup.controls.feeLimit.value, (this.inputFormGroup.controls.rebalanceAmount.value || 0)),
allow_self_payment: true,
last_hop_pubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey,
fromDialog: true
};
this.store.dispatch(sendPayment({ payload }));
}
}

View File

@ -691,31 +691,16 @@ export class LNDEffects implements OnDestroy {
mergeMap((action: { type: string, payload: SendPayment }) => {
this.store.dispatch(openSpinner({ payload: action.payload.uiMessage }));
this.store.dispatch(updateLNDAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.INITIATED } }));
const queryHeaders = {};
queryHeaders['paymentReq'] = action.payload.paymentReq;
if (action.payload.paymentAmount) {
queryHeaders['paymentAmount'] = action.payload.paymentAmount;
}
if (action.payload.outgoingChannel) {
queryHeaders['outgoingChannel'] = action.payload.outgoingChannel.chan_id;
}
if (action.payload.allowSelfPayment) {
queryHeaders['allowSelfPayment'] = action.payload.allowSelfPayment;
} // Channel Rebalancing
if (action.payload.lastHopPubkey) {
queryHeaders['lastHopPubkey'] = action.payload.lastHopPubkey;
}
if (action.payload.feeLimitType && action.payload.feeLimitType !== FEE_LIMIT_TYPES[0].id) {
queryHeaders['feeLimit'] = {};
queryHeaders['feeLimit'][action.payload.feeLimitType] = action.payload.feeLimit;
}
return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.CHANNELS_API + '/transactions', queryHeaders).pipe(
const queryParams = JSON.parse(JSON.stringify(action.payload));
delete queryParams.uiMessage;
delete queryParams.fromDialog;
return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.PAYMENTS_API + '/send', queryParams).pipe(
map((sendRes: any) => {
this.logger.info(sendRes);
this.store.dispatch(closeSpinner({ payload: action.payload.uiMessage }));
this.store.dispatch(updateLNDAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.COMPLETED } }));
if (sendRes.payment_error) {
if (action.payload.allowSelfPayment) {
if (action.payload.allow_self_payment) {
this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: this.invoicesPageSettings?.recordsPerPage, reversed: true } }));
return {
type: LNDActions.SEND_PAYMENT_STATUS_LND,
@ -734,7 +719,7 @@ export class LNDEffects implements OnDestroy {
this.store.dispatch(updateLNDAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.COMPLETED } }));
this.store.dispatch(fetchChannels());
this.store.dispatch(fetchPayments({ payload: { max_payments: this.paymentsPageSettings?.recordsPerPage, reversed: true } }));
if (action.payload.allowSelfPayment) {
if (action.payload.allow_self_payment) {
this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: this.invoicesPageSettings?.recordsPerPage, reversed: true } }));
} else {
let msg = 'Payment Sent Successfully.';
@ -751,7 +736,7 @@ export class LNDEffects implements OnDestroy {
}),
catchError((err: any) => {
this.logger.error('Error: ' + JSON.stringify(err));
if (action.payload.allowSelfPayment) {
if (action.payload.allow_self_payment) {
this.handleErrorWithoutAlert('SendPayment', action.payload.uiMessage, 'Send Payment Failed.', err);
this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: this.invoicesPageSettings?.recordsPerPage, reversed: true } }));
return of({

View File

@ -195,7 +195,7 @@ export class LightningPaymentsComponent implements OnInit, AfterViewInit, OnDest
subscribe((confirmRes) => {
if (confirmRes) {
this.paymentDecoded.num_satoshis = confirmRes[0].inputValue;
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.SEND_PAYMENT, paymentReq: this.paymentRequest, paymentAmount: confirmRes[0].inputValue, fromDialog: false } }));
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.SEND_PAYMENT, payment_request: this.paymentRequest, amp: false, amt: confirmRes[0].inputValue, fromDialog: false } }));
this.resetData();
}
});
@ -224,7 +224,7 @@ export class LightningPaymentsComponent implements OnInit, AfterViewInit, OnDest
pipe(take(1)).
subscribe((confirmRes) => {
if (confirmRes) {
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.SEND_PAYMENT, paymentReq: this.paymentRequest, fromDialog: false } }));
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.SEND_PAYMENT, payment_request: this.paymentRequest, amp: false, fromDialog: false } }));
this.resetData();
}
});

View File

@ -4,7 +4,7 @@
<div fxFlex="95" fxLayoutAlign="start start">
<span class="page-title">Send Payment</span>
</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="11" 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-content class="padding-gap-x-large">
<form #sendPaymentForm="ngForm" fxLayoutAlign="space-between stretch" fxLayout="column">
@ -56,6 +56,7 @@
</mat-autocomplete>
<mat-error *ngIf="selectedChannelCtrl.errors?.notfound">Channel not found in the list.</mat-error>
</mat-form-field>
<mat-slide-toggle fxFlex="25" tabindex="8" color="primary" name="isAmp" [(ngModel)]="isAmp">AMP Payment</mat-slide-toggle>
</div>
</mat-expansion-panel>
<div *ngIf="paymentError !== ''" fxFlex="100" class="alert alert-danger mt-1">
@ -63,8 +64,8 @@
<span *ngIf="paymentError !== ''">{{paymentError}}</span>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="end center">
<button class="mr-1" mat-button color="primary" tabindex="2" type="reset" (click)="resetData()">Clear Fields</button>
<button mat-button id="sendBtn" color="primary" tabindex="3" (click)="onSendPayment()">Send Payment</button>
<button class="mr-1" mat-button color="primary" tabindex="9" type="reset" (click)="resetData()">Clear Fields</button>
<button mat-button id="sendBtn" color="primary" tabindex="10" (click)="onSendPayment()">Send Payment</button>
</div>
</form>
</mat-card-content>

View File

@ -90,8 +90,8 @@ describe('LightningSendPaymentsComponent', () => {
const sendButton = fixture.debugElement.nativeElement.querySelector('#sendBtn');
sendButton.click();
const expectedSendPaymentPayload: SendPayment = {
uiMessage: UI_MESSAGES.SEND_PAYMENT, outgoingChannel: null, feeLimitType: 'none', feeLimit: null, fromDialog: true,
paymentReq: 'lntb4u1psvdzaypp555uks3f6774kl3vdy2dfr00j847pyxtrqelsdnczuxnmtqv99srsdpy23jhxarfdenjqmn8wfuzq3txvejkxarnyq' +
uiMessage: UI_MESSAGES.SEND_PAYMENT, outgoing_chan_ids: undefined, fee_limit_sat: 1000000, fromDialog: true, amp: false,
payment_request: 'lntb4u1psvdzaypp555uks3f6774kl3vdy2dfr00j847pyxtrqelsdnczuxnmtqv99srsdpy23jhxarfdenjqmn8wfuzq3txvejkxarnyq' +
'6qcqp2sp5xjzu6pz2sf8x4v8nmr58kjdm6k05etjfq9c96mwkhzl0g9j7sjkqrzjq28vwprzypa40c75myejm8s2aenkeykcnd7flvy9plp2yjq56nvrc8ss5c' +
'qqqzgqqqqqqqlgqqqqqqgq9q9qy9qsqpt6u4rwfrck3tmpn54kdxjx3xdch62t5wype2f44mmlar07y749xt9elhfhf6dnlfk2tjwg3qpy8njh6remphfcc0630aq' +
'38j0s3hrgpv4eel3'

View File

@ -9,8 +9,8 @@ import { MatDialogRef } from '@angular/material/dialog';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { Node } from '../../../shared/models/RTLconfig';
import { PayRequest, Channel, ChannelsSummary, LightningBalance } from '../../../shared/models/lndModels';
import { APICallStatusEnum, CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, FEE_LIMIT_TYPES, LNDActions, UI_MESSAGES } from '../../../shared/services/consts-enums-functions';
import { PayRequest, Channel, ChannelsSummary, LightningBalance, SendPayment } from '../../../shared/models/lndModels';
import { APICallStatusEnum, CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, FEE_LIMIT_TYPES, LNDActions, UI_MESSAGES, getFeeLimitSat } from '../../../shared/services/consts-enums-functions';
import { CommonService } from '../../../shared/services/common.service';
import { LoggerService } from '../../../shared/services/logger.service';
import { DataService } from '../../../shared/services/data.service';
@ -43,6 +43,7 @@ export class LightningSendPaymentsComponent implements OnInit, OnDestroy {
public activeChannels: Channel[] = [];
public filteredMinAmtActvChannels: Channel[] = [];
public selectedChannelCtrl = new UntypedFormControl();
public isAmp = false;
public feeLimit: number | null = null;
public selFeeLimitType = FEE_LIMIT_TYPES[0];
public feeLimitTypes = FEE_LIMIT_TYPES;
@ -142,10 +143,27 @@ export class LightningSendPaymentsComponent implements OnInit, OnDestroy {
if (!this.paymentDecoded.num_satoshis || this.paymentDecoded.num_satoshis === '' || this.paymentDecoded.num_satoshis === '0') {
this.zeroAmtInvoice = true;
this.paymentDecoded.num_satoshis = this.paymentAmount?.toString() || '';
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.SEND_PAYMENT, paymentReq: this.paymentRequest, paymentAmount: this.paymentAmount || 0, outgoingChannel: this.selectedChannelCtrl.value, feeLimitType: this.selFeeLimitType.id, feeLimit: this.feeLimit, fromDialog: true } }));
const payload: SendPayment = {
uiMessage: UI_MESSAGES.SEND_PAYMENT,
payment_request: this.paymentRequest,
amp: this.isAmp,
amt: this.paymentAmount || 0,
outgoing_chan_ids: this.selectedChannelCtrl.value?.chan_id ? [this.selectedChannelCtrl.value.chan_id] : undefined,
fee_limit_sat: getFeeLimitSat(this.selFeeLimitType.id, this.feeLimit, (this.paymentAmount || 0)),
fromDialog: true
};
this.store.dispatch(sendPayment({ payload }));
} else {
this.zeroAmtInvoice = false;
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.SEND_PAYMENT, paymentReq: this.paymentRequest, outgoingChannel: this.selectedChannelCtrl.value, feeLimitType: this.selFeeLimitType.id, feeLimit: this.feeLimit, fromDialog: true } }));
const payload: SendPayment = {
uiMessage: UI_MESSAGES.SEND_PAYMENT,
payment_request: this.paymentRequest,
amp: this.isAmp,
outgoing_chan_ids: this.selectedChannelCtrl.value?.chan_id ? [this.selectedChannelCtrl.value.chan_id] : undefined,
fee_limit_sat: getFeeLimitSat(this.selFeeLimitType.id, this.feeLimit, (+this.paymentDecoded.num_satoshis || 0)),
fromDialog: true
};
this.store.dispatch(sendPayment({ payload }));
}
}
@ -231,6 +249,7 @@ export class LightningSendPaymentsComponent implements OnInit, OnDestroy {
resetData() {
this.paymentDecoded = {};
this.paymentRequest = '';
this.isAmp = false;
this.selectedChannelCtrl.setValue(null);
this.filteredMinAmtActvChannels = this.activeChannels;
if (this.filteredMinAmtActvChannels.length && this.filteredMinAmtActvChannels.length > 0) {

View File

@ -21,8 +21,8 @@
</ng-container>
<div *ngIf="data.scrollable && shouldScroll" fxLayout="row" fxLayoutAlign="start end" class="btn-sticky-container padding-gap-x-large">
<button mat-mini-fab aria-label="Scroll" fxLayoutAlign="center center" (click)="onScroll()">
<mat-icon *ngIf="scrollDirection === 'DOWN'" fxLayoutAlign="center center">arrow_downward</mat-icon>
<mat-icon *ngIf="scrollDirection === 'UP'" fxLayoutAlign="center center">arrow_upward</mat-icon>
<mat-icon *ngIf="scrollDirection === 'DOWN'" class="arrow-downward" fxLayoutAlign="center center">arrow_downward</mat-icon>
<mat-icon *ngIf="scrollDirection === 'UP'" class="arrow-upward" fxLayoutAlign="center center">arrow_upward</mat-icon>
</button>
</div>
<div fxLayout="row" fxLayoutAlign="end center" class="padding-gap-x-large padding-gap-bottom-large">
@ -50,7 +50,9 @@
<span *ngIf="obj && (!!obj.value || obj.value === 0); else emptyField">
<span class="foreground-secondary-text" fxLayout="row" fxFlex="100" fxLayoutAlign="start stretch" [ngSwitch]="obj.type">
<ng-container *ngSwitchCase="dataTypeEnum.ARRAY">
<span *ngFor="let arrayObj of obj.value" class="display-block w-100" [innerHTML]="arrayObj"></span>
<span fxLayout="column" fxFlex="100">
<span *ngFor="let arrayObj of obj.value" class="display-block w-100" [innerHTML]="arrayObj"></span>
</span>
</ng-container>
<ng-container *ngSwitchCase="dataTypeEnum.DATE_TIME">{{(obj.value * 1000) | date:'dd/MMM/y HH:mm'}}</ng-container>
<ng-container *ngSwitchCase="dataTypeEnum.NUMBER">{{obj.value | number: obj.digitsInfo ? obj.digitsInfo : '1.0-3'}}</ng-container>

View File

@ -552,13 +552,13 @@ export interface FetchPayments {
export interface SendPayment {
uiMessage: string;
fromDialog: boolean;
paymentReq: string;
paymentAmount?: number;
outgoingChannel?: Channel | null;
feeLimitType?: string;
feeLimit?: number | null;
allowSelfPayment?: boolean;
lastHopPubkey?: string;
payment_request: string;
amp: boolean;
amt?: number;
outgoing_chan_ids?: string[] | [];
fee_limit_sat?: number | null;
allow_self_payment?: boolean;
last_hop_pubkey?: string;
}
export interface GetNewAddress {

View File

@ -12,10 +12,11 @@ export function getPaginatorLabel(field: string) {
}
export const HOUR_SECONDS = 3600;
export const SECS_IN_YEAR = 31536000;
export const DEFAULT_INVOICE_EXPIRY = HOUR_SECONDS * 24 * 7;
export const VERSION = '0.15.2-beta';
export const VERSION = '0.15.3-beta';
export const API_URL = isDevMode() ? 'http://localhost:3000/rtl/api' : './api';
@ -69,7 +70,7 @@ export const TRANS_TYPES = [
export const FEE_LIMIT_TYPES = [
{ id: 'none', name: 'No Fee Limit', placeholder: 'No Limit' },
{ id: 'fixed', name: 'Fixed Limit (Sats)', placeholder: 'Fixed Limit in Sats' },
{ id: 'percent', name: 'Percentage of Payment Amount', placeholder: 'Percentage Limit' }
{ id: 'percent', name: 'Percentage of Amount', placeholder: 'Percentage Limit' }
];
export const FEE_RATE_TYPES = [
@ -1396,3 +1397,14 @@ export function getSelectedCurrency(currencyID: string) {
}
return foundCurrency;
}
export function getFeeLimitSat(selFeeLimitTypeID: string, feeLimit: number, amount?: number) {
if (selFeeLimitTypeID === 'fixed') {
return feeLimit;
}
if (selFeeLimitTypeID === 'percent') {
return Math.ceil(((amount || 0) * feeLimit) / 100);
}
return 1000000;
}

View File

@ -402,7 +402,7 @@
&.info-icon-primary {
color: $primary-darker;
}
&.info-icon-text {
&.info-icon-text, &.arrow-downward, &.arrow-upward {
color: $foreground-text;
}
}

View File

@ -323,6 +323,10 @@
&.info-icon-text {
color: $foreground-secondary-text;
}
&.arrow-downward, &.arrow-upward {
font-size: 150%;
color: $background-color;
}
}
}