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-label-var": "error",
"no-restricted-globals": "error", "no-restricted-globals": "error",
"no-undef-init": "error", "no-undef-init": "error",
"no-undefined": "error",
"block-spacing": "error", "block-spacing": "error",
"brace-style": ["error", "1tbs", { "allowSingleLine": true }], "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"comma-style": "error", "comma-style": "error",

View File

@ -2,31 +2,29 @@ name: Artifact
on: on:
push: push:
branches: [ master, 'Release-*' ]
tags: [ 'v*' ] tags: [ 'v*' ]
release: release:
types: [released] 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: workflow_dispatch:
inputs:
version:
description: 'Release version'
required: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v2 uses: actions/cache@v4
id: cache-npm-packages id: cache-npm-packages
with: with:
path: node_modules path: node_modules
@ -37,7 +35,7 @@ jobs:
run: npm ci --legacy-peer-deps run: npm ci --legacy-peer-deps
- name: Cache build frontend - name: Cache build frontend
uses: actions/cache@v2 uses: actions/cache@v4
id: cache-build-frontend id: cache-build-frontend
with: with:
path: frontend path: frontend
@ -47,7 +45,7 @@ jobs:
run: npm run buildfrontend run: npm run buildfrontend
- name: Cache build backend - name: Cache build backend
uses: actions/cache@v2 uses: actions/cache@v4
id: cache-build-backend id: cache-build-backend
with: with:
path: backend path: backend
@ -61,26 +59,32 @@ jobs:
needs: build needs: build
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Cache build frontend - name: Cache build frontend
uses: actions/cache@v2 uses: actions/cache@v4
id: cache-build-frontend id: cache-build-frontend
with: with:
path: frontend path: frontend
key: ${{ runner.os }}-frontend-${{ github.sha }} key: ${{ runner.os }}-frontend-${{ github.sha }}
- name: Cache build backend - name: Cache build backend
uses: actions/cache@v2 uses: actions/cache@v4
id: cache-build-backend id: cache-build-backend
with: with:
path: backend path: backend
key: ${{ runner.os }}-backend-${{ github.sha }} key: ${{ runner.os }}-backend-${{ github.sha }}
- name: Compress files - name: Compress files
run: tar -czf /tmp/rtlbuild.tar.gz frontend backend rtl.js package.json package-lock.json env:
VERSION: "${{ github.event.release.tag_name || github.event.inputs.version || '' }}"
- uses: actions/upload-artifact@v2 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: with:
name: rtl-build-${{ github.event.release.tag_name }} name: rtl-build-${{ github.event.release.tag_name || github.event.inputs.version || '' }}
path: /tmp/rtlbuild.tar.gz 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) { if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: 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; options.body = req.body;
request.post(options).then((body) => { request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoices Deleted', data: 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) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'Delete Invoice Error', req.session.selectedNode); const err = common.handleError(errRes, 'Invoice', 'Delete Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error }); 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) { if (options.error) {
return res.status(options.statusCode).json({ message: options.message, error: 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; options.body = req.body;
request.post(options).then((body) => { request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Disabled', data: 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); channelNodeIds = channelNodeIds.substring(1);
options.url = selNode.settings.lnServerUrl + '/nodes'; 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 }); logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Node Ids to find alias', data: channelNodeIds });
return request.post(options).then((nodes) => { return request.post(options).then((nodes) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Filtered Nodes Received', data: 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 }); 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) => { export const closeChannel = (req, res, next) => {
try { try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' }); 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 }); 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'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; 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(); const router = Router();
router.get('/', isAuthenticated, getAllChannels); router.get('/', isAuthenticated, getAllChannels);
router.get('/pending', isAuthenticated, getPendingChannels); router.get('/pending', isAuthenticated, getPendingChannels);
router.get('/closed', isAuthenticated, getClosedChannels); router.get('/closed', isAuthenticated, getClosedChannels);
router.post('/', isAuthenticated, postChannel); router.post('/', isAuthenticated, postChannel);
router.post('/transactions', isAuthenticated, postTransactions);
router.delete('/:channelPoint', isAuthenticated, closeChannel); router.delete('/:channelPoint', isAuthenticated, closeChannel);
router.post('/chanPolicy', isAuthenticated, postChanPolicy); router.post('/chanPolicy', isAuthenticated, postChanPolicy);
export default router; export default router;

View File

@ -1,11 +1,12 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; 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(); const router = Router();
router.get('/', isAuthenticated, getPayments); router.get('/', isAuthenticated, getPayments);
router.get('/alltransactions', isAuthenticated, getAllLightningTransactions); router.get('/alltransactions', isAuthenticated, getAllLightningTransactions);
router.get('/decode/:payRequest', isAuthenticated, decodePayment); router.get('/decode/:payRequest', isAuthenticated, decodePayment);
router.get('/lookup/:paymentHash', isAuthenticated, paymentLookup); router.get('/lookup/:paymentHash', isAuthenticated, paymentLookup);
router.post('/', isAuthenticated, decodePayments); router.post('/', isAuthenticated, decodePayments);
router.post('/send', isAuthenticated, sendPayment);
export default router; 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); const exists = fs.existsSync(this.appConfig.SSO.rtlCookiePath);
if (exists) { if (exists) {
try { try {

View File

@ -146,7 +146,7 @@ export class ConfigService {
this.common.nodes[idx] = { settings: { blockExplorerUrl: '' }, authentication: {} }; this.common.nodes[idx] = { settings: { blockExplorerUrl: '' }, authentication: {} };
this.common.nodes[idx].index = node.index; this.common.nodes[idx].index = node.index;
this.common.nodes[idx].lnNode = node.lnNode; 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') { if (this.common.nodes[idx].lnImplementation === 'CLT') {
this.common.nodes[idx].lnImplementation = 'CLN'; this.common.nodes[idx].lnImplementation = 'CLN';
} }
@ -343,7 +343,7 @@ export class ConfigService {
this.errMsg = 'Please set rtlCookiePath value for single sign on option!'; this.errMsg = 'Please set rtlCookiePath value for single sign on option!';
} }
else { 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: Create an environment file with your required configurations. Sample .env:
``` ```
RTL_CONFIG_PATH=/RTLConfig RTL_CONFIG_PATH=/RTLConfig
LN_IMPLEMENTATION=LND
MACAROON_PATH=/LNDMacaroon MACAROON_PATH=/LNDMacaroon
LN_SERVER_URL=https://host.docker.internal:8080 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 @angular/animations
MIT 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 @angular/cdk
MIT MIT
@ -28,15 +50,81 @@ THE SOFTWARE.
@angular/common @angular/common
MIT 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 @angular/core
MIT 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 @angular/flex-layout
MIT MIT
@angular/forms @angular/forms
MIT 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 @angular/material
MIT MIT
@ -65,9 +153,53 @@ THE SOFTWARE.
@angular/platform-browser @angular/platform-browser
MIT 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 @angular/router
MIT 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 @babel/runtime
MIT MIT
@ -1565,9 +1697,6 @@ THE SOFTWARE.
elliptic elliptic
MIT MIT
encode-utf8
MIT
events events
MIT MIT
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", "name": "rtl",
"version": "0.15.2-beta", "version": "0.15.3-beta",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "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..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoices', msg: 'Deleting Expired Invoices..' });
options = common.getOptions(req); options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); } 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; options.body = req.body;
request.post(options).then((body) => { request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Invoice', msg: 'Invoices Deleted', data: 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) => { }).catch((errRes) => {
const err = common.handleError(errRes, 'Invoice', 'Delete Invoice Error', req.session.selectedNode); const err = common.handleError(errRes, 'Invoice', 'Delete Invoice Error', req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error }); 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..' }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Disabling Offer..' });
options = common.getOptions(req); options = common.getOptions(req);
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); } 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; options.body = req.body;
request.post(options).then((body) => { request.post(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Offers', msg: 'Offer Disabled', data: 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); channelNodeIds = channelNodeIds.substring(1);
options.url = selNode.settings.lnServerUrl + '/nodes'; 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 }); logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Node Ids to find alias', data: channelNodeIds });
return request.post(options).then((nodes) => { return request.post(options).then((nodes) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Filtered Nodes Received', data: 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) => { export const closeChannel = (req, res, next) => {
try { try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' }); 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 }); 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'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; 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(); const router = Router();
@ -9,7 +9,6 @@ router.get('/', isAuthenticated, getAllChannels);
router.get('/pending', isAuthenticated, getPendingChannels); router.get('/pending', isAuthenticated, getPendingChannels);
router.get('/closed', isAuthenticated, getClosedChannels); router.get('/closed', isAuthenticated, getClosedChannels);
router.post('/', isAuthenticated, postChannel); router.post('/', isAuthenticated, postChannel);
router.post('/transactions', isAuthenticated, postTransactions);
router.delete('/:channelPoint', isAuthenticated, closeChannel); router.delete('/:channelPoint', isAuthenticated, closeChannel);
router.post('/chanPolicy', isAuthenticated, postChanPolicy); router.post('/chanPolicy', isAuthenticated, postChanPolicy);

View File

@ -1,7 +1,7 @@
import exprs from 'express'; import exprs from 'express';
const { Router } = exprs; const { Router } = exprs;
import { isAuthenticated } from '../../utils/authCheck.js'; 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(); const router = Router();
@ -10,5 +10,6 @@ router.get('/alltransactions', isAuthenticated, getAllLightningTransactions);
router.get('/decode/:payRequest', isAuthenticated, decodePayment); router.get('/decode/:payRequest', isAuthenticated, decodePayment);
router.get('/lookup/:paymentHash', isAuthenticated, paymentLookup); router.get('/lookup/:paymentHash', isAuthenticated, paymentLookup);
router.post('/', isAuthenticated, decodePayments); router.post('/', isAuthenticated, decodePayments);
router.post('/send', isAuthenticated, sendPayment);
export default router; 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); const exists = fs.existsSync(this.appConfig.SSO.rtlCookiePath);
if (exists) { if (exists) {
try { try {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 { ErrorMessageComponent } from '../../shared/components/data-modal/error-message/error-message.component';
import { CLNInvoiceInformationComponent } from '../transactions/invoices/invoice-information-modal/invoice-information.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 { 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 { closeAllDialogs, closeSpinner, logout, openAlert, openSnackBar, openSpinner, setApiUrl, setNodeData } from '../../store/rtl.actions';
import { RTLState } from '../../store/rtl.state'; import { RTLState } from '../../store/rtl.state';
@ -114,8 +114,8 @@ export class CLNEffects implements OnDestroy {
}), }),
catchError((err) => { catchError((err) => {
const code = this.commonService.extractErrorCode(err); const code = this.commonService.extractErrorCode(err);
const msg = (code === 'ETIMEDOUT') ? 'Unable to Connect to Core Lightning Server.' : this.commonService.extractErrorMessage(err); const msg = (code === 503) ? 'Unable to Connect to Core Lightning Server.' : this.commonService.extractErrorMessage(err);
this.router.navigate(['/login'], { state: { logoutReason: JSON.stringify(msg) } }); 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 }); this.handleErrorWithoutAlert('FetchInfo', UI_MESSAGES.GET_NODE_INFO, 'Fetching Node Info Failed.', { status: code, error: msg });
return of({ type: RTLActions.VOID }); return of({ type: RTLActions.VOID });
}) })
@ -608,12 +608,12 @@ export class CLNEffects implements OnDestroy {
ofType(CLNActions.DELETE_EXPIRED_INVOICE_CLN), ofType(CLNActions.DELETE_EXPIRED_INVOICE_CLN),
mergeMap((action: { type: string, payload: number }) => { mergeMap((action: { type: string, payload: number }) => {
this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DELETE_INVOICE })); this.store.dispatch(openSpinner({ payload: UI_MESSAGES.DELETE_INVOICE }));
return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.INVOICES_API + '/delete', { maxexpiry: action.payload }). return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.INVOICES_API + '/delete', { subsystem: 'expiredinvoices', age: SECS_IN_YEAR }).
pipe( pipe(
map((postRes: any) => { map((postRes: any) => {
this.logger.info(postRes); this.logger.info(postRes);
this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DELETE_INVOICE })); this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.DELETE_INVOICE }));
this.store.dispatch(openSnackBar({ payload: 'Invoices Deleted Successfully!' })); this.store.dispatch(openSnackBar({ payload: postRes.status }));
return { type: CLNActions.FETCH_INVOICES_CLN }; return { type: CLNActions.FETCH_INVOICES_CLN };
}), }),
catchError((err: any) => { catchError((err: any) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@
<mat-option (click)="onUTXOClick(utxo)">View Info</mat-option> <mat-option (click)="onUTXOClick(utxo)">View Info</mat-option>
<mat-option (click)="onLabelUTXO(utxo)">Label</mat-option> <mat-option (click)="onLabelUTXO(utxo)">Label</mat-option>
<mat-option (click)="onLeaseUTXO(utxo)">Lease</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> </mat-select>
</div> </div>
</td> </td>

View File

@ -12,8 +12,8 @@ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { ChannelRebalanceAlert } from '../../../../shared/models/alertData'; import { ChannelRebalanceAlert } from '../../../../shared/models/alertData';
import { LoggerService } from '../../../../shared/services/logger.service'; import { LoggerService } from '../../../../shared/services/logger.service';
import { CommonService } from '../../../../shared/services/common.service'; import { CommonService } from '../../../../shared/services/common.service';
import { Channel, QueryRoutes, ListInvoices } from '../../../../shared/models/lndModels'; import { Channel, QueryRoutes, ListInvoices, SendPayment } from '../../../../shared/models/lndModels';
import { DEFAULT_INVOICE_EXPIRY, FEE_LIMIT_TYPES, LNDActions, PAGE_SIZE, ScreenSizeEnum, UI_MESSAGES } from '../../../../shared/services/consts-enums-functions'; 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 { RTLState } from '../../../../store/rtl.state';
import { saveNewInvoice, sendPayment } from '../../../store/lnd.actions'; import { saveNewInvoice, sendPayment } from '../../../store/lnd.actions';
@ -239,13 +239,29 @@ export class ChannelRebalanceComponent implements OnInit, OnDestroy {
this.flgInvoiceGenerated = true; this.flgInvoiceGenerated = true;
this.paymentRequest = payReq; this.paymentRequest = payReq;
if (this.feeFormGroup.controls.selFeeLimitType.value.id === 'percent' && !(+this.feeFormGroup.controls.feeLimit.value % 1 === 0)) { 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, const payload: SendPayment = {
feeLimitType: 'fixed', feeLimit: Math.ceil((+this.feeFormGroup.controls.feeLimit.value * +this.inputFormGroup.controls.rebalanceAmount.value) / 100), uiMessage: UI_MESSAGES.NO_SPINNER,
allowSelfPayment: true, lastHopPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey, fromDialog: true } })); 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 { } else {
this.store.dispatch(sendPayment({ payload: { uiMessage: UI_MESSAGES.NO_SPINNER, paymentReq: payReq, outgoingChannel: this.selChannel, const payload: SendPayment = {
feeLimitType: this.feeFormGroup.controls.selFeeLimitType.value.id, feeLimit: this.feeFormGroup.controls.feeLimit.value, allowSelfPayment: true, uiMessage: UI_MESSAGES.NO_SPINNER,
lastHopPubkey: this.inputFormGroup.controls.selRebalancePeer.value.remote_pubkey, fromDialog: true } })); 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 }) => { mergeMap((action: { type: string, payload: SendPayment }) => {
this.store.dispatch(openSpinner({ payload: action.payload.uiMessage })); this.store.dispatch(openSpinner({ payload: action.payload.uiMessage }));
this.store.dispatch(updateLNDAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.INITIATED } })); this.store.dispatch(updateLNDAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.INITIATED } }));
const queryHeaders = {}; const queryParams = JSON.parse(JSON.stringify(action.payload));
queryHeaders['paymentReq'] = action.payload.paymentReq; delete queryParams.uiMessage;
if (action.payload.paymentAmount) { delete queryParams.fromDialog;
queryHeaders['paymentAmount'] = action.payload.paymentAmount; return this.httpClient.post(this.CHILD_API_URL + API_END_POINTS.PAYMENTS_API + '/send', queryParams).pipe(
}
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(
map((sendRes: any) => { map((sendRes: any) => {
this.logger.info(sendRes); this.logger.info(sendRes);
this.store.dispatch(closeSpinner({ payload: action.payload.uiMessage })); this.store.dispatch(closeSpinner({ payload: action.payload.uiMessage }));
this.store.dispatch(updateLNDAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.COMPLETED } })); this.store.dispatch(updateLNDAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.COMPLETED } }));
if (sendRes.payment_error) { 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 } })); this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: this.invoicesPageSettings?.recordsPerPage, reversed: true } }));
return { return {
type: LNDActions.SEND_PAYMENT_STATUS_LND, 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(updateLNDAPICallStatus({ payload: { action: 'SendPayment', status: APICallStatusEnum.COMPLETED } }));
this.store.dispatch(fetchChannels()); this.store.dispatch(fetchChannels());
this.store.dispatch(fetchPayments({ payload: { max_payments: this.paymentsPageSettings?.recordsPerPage, reversed: true } })); 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 } })); this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: this.invoicesPageSettings?.recordsPerPage, reversed: true } }));
} else { } else {
let msg = 'Payment Sent Successfully.'; let msg = 'Payment Sent Successfully.';
@ -751,7 +736,7 @@ export class LNDEffects implements OnDestroy {
}), }),
catchError((err: any) => { catchError((err: any) => {
this.logger.error('Error: ' + JSON.stringify(err)); 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.handleErrorWithoutAlert('SendPayment', action.payload.uiMessage, 'Send Payment Failed.', err);
this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: this.invoicesPageSettings?.recordsPerPage, reversed: true } })); this.store.dispatch(fetchInvoices({ payload: { num_max_invoices: this.invoicesPageSettings?.recordsPerPage, reversed: true } }));
return of({ return of({

View File

@ -195,7 +195,7 @@ export class LightningPaymentsComponent implements OnInit, AfterViewInit, OnDest
subscribe((confirmRes) => { subscribe((confirmRes) => {
if (confirmRes) { if (confirmRes) {
this.paymentDecoded.num_satoshis = confirmRes[0].inputValue; 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(); this.resetData();
} }
}); });
@ -224,7 +224,7 @@ export class LightningPaymentsComponent implements OnInit, AfterViewInit, OnDest
pipe(take(1)). pipe(take(1)).
subscribe((confirmRes) => { subscribe((confirmRes) => {
if (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(); this.resetData();
} }
}); });

View File

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

View File

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

View File

@ -9,8 +9,8 @@ import { MatDialogRef } from '@angular/material/dialog';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { Node } from '../../../shared/models/RTLconfig'; import { Node } from '../../../shared/models/RTLconfig';
import { PayRequest, Channel, ChannelsSummary, LightningBalance } from '../../../shared/models/lndModels'; import { PayRequest, Channel, ChannelsSummary, LightningBalance, SendPayment } from '../../../shared/models/lndModels';
import { APICallStatusEnum, CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, FEE_LIMIT_TYPES, LNDActions, UI_MESSAGES } from '../../../shared/services/consts-enums-functions'; 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 { CommonService } from '../../../shared/services/common.service';
import { LoggerService } from '../../../shared/services/logger.service'; import { LoggerService } from '../../../shared/services/logger.service';
import { DataService } from '../../../shared/services/data.service'; import { DataService } from '../../../shared/services/data.service';
@ -43,6 +43,7 @@ export class LightningSendPaymentsComponent implements OnInit, OnDestroy {
public activeChannels: Channel[] = []; public activeChannels: Channel[] = [];
public filteredMinAmtActvChannels: Channel[] = []; public filteredMinAmtActvChannels: Channel[] = [];
public selectedChannelCtrl = new UntypedFormControl(); public selectedChannelCtrl = new UntypedFormControl();
public isAmp = false;
public feeLimit: number | null = null; public feeLimit: number | null = null;
public selFeeLimitType = FEE_LIMIT_TYPES[0]; public selFeeLimitType = FEE_LIMIT_TYPES[0];
public feeLimitTypes = FEE_LIMIT_TYPES; 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') { if (!this.paymentDecoded.num_satoshis || this.paymentDecoded.num_satoshis === '' || this.paymentDecoded.num_satoshis === '0') {
this.zeroAmtInvoice = true; this.zeroAmtInvoice = true;
this.paymentDecoded.num_satoshis = this.paymentAmount?.toString() || ''; 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 { } else {
this.zeroAmtInvoice = false; 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() { resetData() {
this.paymentDecoded = {}; this.paymentDecoded = {};
this.paymentRequest = ''; this.paymentRequest = '';
this.isAmp = false;
this.selectedChannelCtrl.setValue(null); this.selectedChannelCtrl.setValue(null);
this.filteredMinAmtActvChannels = this.activeChannels; this.filteredMinAmtActvChannels = this.activeChannels;
if (this.filteredMinAmtActvChannels.length && this.filteredMinAmtActvChannels.length > 0) { if (this.filteredMinAmtActvChannels.length && this.filteredMinAmtActvChannels.length > 0) {

View File

@ -21,8 +21,8 @@
</ng-container> </ng-container>
<div *ngIf="data.scrollable && shouldScroll" fxLayout="row" fxLayoutAlign="start end" class="btn-sticky-container padding-gap-x-large"> <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()"> <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 === 'DOWN'" class="arrow-downward" fxLayoutAlign="center center">arrow_downward</mat-icon>
<mat-icon *ngIf="scrollDirection === 'UP'" fxLayoutAlign="center center">arrow_upward</mat-icon> <mat-icon *ngIf="scrollDirection === 'UP'" class="arrow-upward" fxLayoutAlign="center center">arrow_upward</mat-icon>
</button> </button>
</div> </div>
<div fxLayout="row" fxLayoutAlign="end center" class="padding-gap-x-large padding-gap-bottom-large"> <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 *ngIf="obj && (!!obj.value || obj.value === 0); else emptyField">
<span class="foreground-secondary-text" fxLayout="row" fxFlex="100" fxLayoutAlign="start stretch" [ngSwitch]="obj.type"> <span class="foreground-secondary-text" fxLayout="row" fxFlex="100" fxLayoutAlign="start stretch" [ngSwitch]="obj.type">
<ng-container *ngSwitchCase="dataTypeEnum.ARRAY"> <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>
<ng-container *ngSwitchCase="dataTypeEnum.DATE_TIME">{{(obj.value * 1000) | date:'dd/MMM/y HH:mm'}}</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> <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 { export interface SendPayment {
uiMessage: string; uiMessage: string;
fromDialog: boolean; fromDialog: boolean;
paymentReq: string; payment_request: string;
paymentAmount?: number; amp: boolean;
outgoingChannel?: Channel | null; amt?: number;
feeLimitType?: string; outgoing_chan_ids?: string[] | [];
feeLimit?: number | null; fee_limit_sat?: number | null;
allowSelfPayment?: boolean; allow_self_payment?: boolean;
lastHopPubkey?: string; last_hop_pubkey?: string;
} }
export interface GetNewAddress { export interface GetNewAddress {

View File

@ -12,10 +12,11 @@ export function getPaginatorLabel(field: string) {
} }
export const HOUR_SECONDS = 3600; export const HOUR_SECONDS = 3600;
export const SECS_IN_YEAR = 31536000;
export const DEFAULT_INVOICE_EXPIRY = HOUR_SECONDS * 24 * 7; 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'; export const API_URL = isDevMode() ? 'http://localhost:3000/rtl/api' : './api';
@ -69,7 +70,7 @@ export const TRANS_TYPES = [
export const FEE_LIMIT_TYPES = [ export const FEE_LIMIT_TYPES = [
{ id: 'none', name: 'No Fee Limit', placeholder: 'No Limit' }, { id: 'none', name: 'No Fee Limit', placeholder: 'No Limit' },
{ id: 'fixed', name: 'Fixed Limit (Sats)', placeholder: 'Fixed Limit in Sats' }, { 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 = [ export const FEE_RATE_TYPES = [
@ -1396,3 +1397,14 @@ export function getSelectedCurrency(currencyID: string) {
} }
return foundCurrency; 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 { &.info-icon-primary {
color: $primary-darker; color: $primary-darker;
} }
&.info-icon-text { &.info-icon-text, &.arrow-downward, &.arrow-upward {
color: $foreground-text; color: $foreground-text;
} }
} }

View File

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