Add AMP toggle for LND Send Payments

This commit is contained in:
ShahanaFarooqui 2024-11-10 15:22:29 -08:00
parent 417fbb3e84
commit 61c1a2888e
26 changed files with 154 additions and 136 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(()=>{"use strict";var e,v={},m={};function r(e){var 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))})()})();

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],s=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(s=!1,f<a&&(a=f));if(s){e.splice(n--,1);var d=i();void 0!==d&&(o=d)}}return o}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,i,f]},r.d=(e,o)=>{for(var t in o)r.o(o,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((o,t)=>(r.f[t](e,o),o),[])),r.u=e=>e+"."+{17:"6fa7154eb6e447e2",190:"8009411a512731e5",193:"0e1a81316bbc29da",853:"50b06a24091d386f"}[e]+".js",r.miniCssF=e=>{},r.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),(()=>{var e={},o="RTLApp:";r.l=(t,i,f,n)=>{if(e[t])e[t].push(i);else{var a,s;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==o+f){a=u;break}}a||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",o+f),a.src=r.tu(t)),e[t]=[i];var c=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(y=>y(b)),g)return g(b)},p=setTimeout(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:o=>o},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={121:0};r.f.j=(i,f)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)f.push(n[2]);else if(121!=i){var a=new Promise((u,c)=>n=e[i]=[u,c]);f.push(n[2]=a);var s=r.p+r.u(i),l=new Error;r.l(s,u=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var c=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+i+" failed.\n("+c+": "+p+")",l.name="ChunkLoadError",l.type=c,l.request=p,n[1](l)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var o=(i,f)=>{var l,d,[n,a,s]=f,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(s)var c=s(r)}for(i&&i(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[d]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))})()})();

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "rtl",
"version": "0.15.2-beta",
"version": "0.15.3-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rtl",
"version": "0.15.2-beta",
"version": "0.15.3-beta",
"license": "MIT",
"dependencies": {
"@ngrx/effects": "^17.2.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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