Loop in and loop out (#279)

Loop in and loop out
pull/294/head^2
ShahanaFarooqui 4 years ago committed by GitHub
parent ee797481b4
commit 86d7d1ffa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitignore vendored

@ -48,3 +48,4 @@ RTL-Multi-Node-Conf.json
RTL.conf
RTL-1.conf
RTL-Multi-Node-Conf-1.json
RTL-Config-for-BTC-Testing.json

@ -3,7 +3,7 @@
<a href="https://snyk.io/test/github/Ride-The-Lightning/RTL"><img src="https://snyk.io/test/github/Ride-The-Lightning/RTL/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/Ride-The-Lightning/RTL" style="max-width:100%;"></a>
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE)
### Stable Release: v0.6.4
### Stable Release: v0.6.8
**Intro** -- [Application Features](docs/Application_features.md) -- [Road Map](docs/Roadmap.md) -- [LND API Coverage](docs/LNDAPICoverage.md) -- [Application Configurations](docs/Application_configurations) -- [C-lightning](docs/C-Lightning-setup.md)

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

@ -12,8 +12,8 @@
<link rel="mask-icon" href="assets/images/favicon-light/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="styles.d944e28a09351c044329.css"></head>
<link rel="stylesheet" href="styles.f04e677f00c07a00a61f.css"></head>
<body>
<rtl-app></rtl-app>
<script src="runtime.d324cb9ce055cdfad42a.js" defer></script><script src="polyfills-es5.2ae7ace69949ec0a3f00.js" nomodule defer></script><script src="polyfills.3302e98effc5e50a54c2.js" defer></script><script src="main.6078c58378efda053404.js" defer></script></body>
<script src="runtime.f077d9a5a55b365fad3b.js" defer></script><script src="polyfills-es5.2ae7ace69949ec0a3f00.js" nomodule defer></script><script src="polyfills.3302e98effc5e50a54c2.js" defer></script><script src="main.0b4165691223413d29d4.js" defer></script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],f=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(l&&l(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"7265422b6fd577fc6798",6:"ba6a125e1b449192ae1c",7:"7189929795574f12346d"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(f);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var f=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var f=0;f<i.length;f++)r(i[f]);var l=c;t()}([]);
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"55621ac84bdf6c786e25",6:"83056549d4d88d5a89c1",7:"75677e99073742b9f2a4"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -26,6 +26,7 @@ const payReqRoutes = require("./routes/lnd/payReq");
const paymentsRoutes = require("./routes/lnd/payments");
const invoiceRoutes = require("./routes/lnd/invoices");
const switchRoutes = require("./routes/lnd/switch");
const loopRoutes = require('./routes/lnd/loop');
const messageRoutes = require("./routes/lnd/message");
const infoCLRoutes = require("./routes/c-lightning/getInfo");
@ -75,6 +76,7 @@ app.use(apiLNDRoot + "payreq", payReqRoutes);
app.use(apiLNDRoot + "payments", paymentsRoutes);
app.use(apiLNDRoot + "invoices", invoiceRoutes);
app.use(apiLNDRoot + "switch", switchRoutes);
app.use(apiLNDRoot + "loop", loopRoutes);
app.use(apiLNDRoot + "message", messageRoutes);
app.use(apiCLRoot + "getinfo", infoCLRoutes);

@ -14,6 +14,10 @@ common.secret_key = crypto.randomBytes(64).toString('hex');
common.nodes = [];
common.selectedNode = {};
common.getSelSwapServerUrl = () => {
return common.selectedNode.swap_server_url;
};
common.getSelLNServerUrl = () => {
return common.selectedNode.ln_server_url;
};

@ -64,6 +64,7 @@ connect.setDefaultConfig = () => {
channelBackupPath: channelBackupPath,
enableLogging: false,
lnServerUrl: "https://localhost:8080/v1",
swapServerUrl: "https://localhost:8081/v1",
fiatConversion: false
}
}
@ -152,6 +153,7 @@ connect.validateNodeConfig = (config) => {
} else {
common.nodes[idx].config_path = '';
}
common.nodes[idx].swap_server_url = process.env.SWAP_SERVER_URL ? process.env.SWAP_SERVER_URL : (node.Settings.swapServerUrl) ? node.Settings.swapServerUrl.trim() : '';
common.nodes[idx].bitcoind_config_path = process.env.BITCOIND_CONFIG_PATH ? process.env.BITCOIND_CONFIG_PATH : (node.Settings.bitcoindConfigPath) ? node.Settings.bitcoindConfigPath : '';
common.nodes[idx].enable_logging = (node.Settings.enableLogging) ? !!node.Settings.enableLogging : false;
common.nodes[idx].channel_backup_path = process.env.CHANNEL_BACKUP_PATH ? process.env.CHANNEL_BACKUP_PATH : (node.Settings.channelBackupPath) ? node.Settings.channelBackupPath : common.rtl_conf_file_path + common.path_separator + 'backup' + common.path_separator + 'node-' + node.index;

@ -48,6 +48,7 @@ exports.getRTLConfig = (req, res, next) => {
settings.bitcoindConfigPath = node.bitcoind_config_path;
settings.enableLogging = node.enable_logging ? !!node.enable_logging : false;
settings.lnServerUrl = node.ln_server_url;
settings.swapServerUrl = node.swap_server_url;
settings.channelBackupPath = node.channel_backup_path;
settings.currencyUnit = node.currency_unit;
nodesArr.push({

@ -0,0 +1,267 @@
var request = require('request-promise');
var common = require('../../common');
var logger = require('../logger');
var options = {};
var swapServerUrl = '';
exports.loopOut = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/out';
let body = {
amt: req.body.amount,
sweep_conf_target: req.body.targetConf,
max_swap_routing_fee: req.body.swapRoutingFee,
max_miner_fee: req.body.minerFee,
max_prepay_routing_fee: req.body.prepayRoutingFee,
max_prepay_amt: req.body.prepayAmt,
max_swap_fee: req.body.swapFee,
swap_publication_deadline: req.body.swapPublicationDeadline
};
if (req.body.chanId !== '') { body['loop_out_channel'] = req.body.chanId; }
if (req.body.destAddress !== '') { body['dest'] = req.body.destAddress; }
options.body = JSON.stringify(body);
logger.info({fileName: 'Loop', msg: 'Loop Out Body: ' + options.body});
request.post(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Out: ' + JSON.stringify(body)});
if(!body || body.error) {
res.status(500).json({
message: 'Loop Out Failed!',
error: (!body) ? 'Error From Server!' : body.error.message
});
} else {
res.status(201).json(body);
}
})
.catch(function (err) {
logger.error({fileName: 'Loop Out', lineNum: 33, msg: 'Loop Out Failed: ' + JSON.stringify(err.error)});
return res.status(500).json({
message: "Loop Out Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.loopOutTerms = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Terms Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/out/terms';
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Out Terms: ' + JSON.stringify(body)});
res.status(200).json(body);
})
.catch((err) => {
return res.status(500).json({
message: "Loop Out Terms Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.loopOutQuote = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Quote Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/out/quote/' + req.params.amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
logger.info({fileName: 'Loop', msg: 'Loop Out Quote URL: ' + options.url});
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Out Quote: ' + body});
body = JSON.parse(body);
body.amount = +req.params.amount;
body.swap_payment_dest = body.swap_payment_dest ? Buffer.from(body.swap_payment_dest, 'base64').toString('hex') : '';
res.status(200).json(body);
})
.catch((err) => {
return res.status(500).json({
message: "Loop Out Quote Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.loopOutTermsAndQuotes = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Terms And Quotes Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/out/terms';
request(options).then(function(terms) {
logger.info({fileName: 'Loop', msg: 'Loop Out Terms: ' + JSON.stringify(terms)});
const options1 = {}; const options2 = {};
terms = JSON.parse(terms);
options1.url = swapServerUrl + '/loop/out/quote/' + terms.min_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
options2.url = swapServerUrl + '/loop/out/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
logger.info({fileName: 'Loop', msg: 'Loop Out Min Quote Options: ' + JSON.stringify(options1)});
logger.info({fileName: 'Loop', msg: 'Loop Out Max Quote Options: ' + JSON.stringify(options2)});
Promise.all([request(options1), request(options2)]).then(function(values) {
values[0] = JSON.parse(values[0]);
values[1] = JSON.parse(values[1]);
values[0].amount = +terms.min_swap_amount;
values[1].amount = +terms.max_swap_amount;
values[0].swap_payment_dest = values[0].swap_payment_dest ? Buffer.from(values[0].swap_payment_dest, 'base64').toString('hex') : '';
values[1].swap_payment_dest = values[1].swap_payment_dest ? Buffer.from(values[1].swap_payment_dest, 'base64').toString('hex') : '';
logger.info({fileName: 'Loop', msg: 'Loop Out Quotes 1: ' + JSON.stringify(values[0])});
logger.info({fileName: 'Loop', msg: 'Loop Out Quotes 2: ' + JSON.stringify(values[1])});
res.status(200).json(values);
})
.catch((err) => {
return res.status(500).json({
message: "Loop Out Quotes Failed!",
error: err.error.error ? err.error.error : err.error
});
});
})
.catch((err) => {
return res.status(500).json({
message: "Loop Out Terms Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.loopIn = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop In Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/in';
options.body = JSON.stringify({
amt: req.body.amount,
max_swap_fee: req.body.swapFee,
max_miner_fee: req.body.minerFee
// last_hop: req.body.lastHop,
// external_htlc: req.body.externalHtlc
});
request.post(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In: ' + JSON.stringify(body)});
if(!body || body.error) {
res.status(500).json({
message: 'Loop In Failed!',
error: (!body) ? 'Error From Server!' : body.error.message
});
} else {
res.status(201).json(body);
}
})
.catch(function (err) {
logger.error({fileName: 'Loop In', lineNum: 134, msg: 'Loop In Failed: ' + JSON.stringify(err.error)});
return res.status(500).json({
message: "Loop In Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.loopInTerms = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop In Terms Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/in/terms';
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In Terms: ' + JSON.stringify(body)});
res.status(200).json(body);
})
.catch((err) => {
return res.status(500).json({
message: "Loop In Terms Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.loopInQuote = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop In Quote Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/in/quote/' + req.params.amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
logger.info({fileName: 'Loop', msg: 'Loop In Quote Options: ' + options.url});
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop In Quote: ' + JSON.stringify(body)});
body = JSON.parse(body);
body.amount = +req.params.amount;
body.swap_payment_dest = body.swap_payment_dest ? Buffer.from(body.swap_payment_dest, 'base64').toString('hex') : '';
res.status(200).json(body);
})
.catch((err) => {
return res.status(500).json({
message: "Loop In Quote Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.loopInTermsAndQuotes = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop In Terms And Quotes Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/in/terms';
request(options).then(function(terms) {
logger.info({fileName: 'Loop', msg: 'Loop In Terms: ' + JSON.stringify(terms)});
const options1 = {}; const options2 = {};
terms = JSON.parse(terms);
options1.url = swapServerUrl + '/loop/in/quote/' + terms.min_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
options2.url = swapServerUrl + '/loop/in/quote/' + terms.max_swap_amount + '?conf_target=' + (req.query.targetConf ? req.query.targetConf : '2') + '&swap_publication_deadline=' + req.query.swapPublicationDeadline;
logger.info({fileName: 'Loop', msg: 'Loop In Min Quote Options: ' + JSON.stringify(options1)});
logger.info({fileName: 'Loop', msg: 'Loop In Max Quote Options: ' + JSON.stringify(options2)});
Promise.all([request(options1), request(options2)]).then(function(values) {
values[0] = JSON.parse(values[0]);
values[1] = JSON.parse(values[1]);
values[0].amount = +terms.min_swap_amount;
values[1].amount = +terms.max_swap_amount;
values[0].swap_payment_dest = values[0].swap_payment_dest ? Buffer.from(values[0].swap_payment_dest, 'base64').toString('hex') : '';
values[1].swap_payment_dest = values[1].swap_payment_dest ? Buffer.from(values[1].swap_payment_dest, 'base64').toString('hex') : '';
logger.info({fileName: 'Loop', msg: 'Loop In Quotes 1: ' + JSON.stringify(values[0])});
logger.info({fileName: 'Loop', msg: 'Loop In Quotes 2: ' + JSON.stringify(values[1])});
res.status(200).json(values);
})
.catch((err) => {
return res.status(500).json({
message: "Loop In Quotes Failed!",
error: err.error.error ? err.error.error : err.error
});
});
})
.catch((err) => {
return res.status(500).json({
message: "Loop In Terms Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.swaps = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/swaps';
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Swaps: ' + body});
body = JSON.parse(body);
if (body.swaps && body.swaps.length > 0) {
body.swaps.forEach(swap => {
swap.initiation_time_str = (!swap.initiation_time) ? '' : common.convertTimestampToDate(Math.round(swap.initiation_time/1000000000));
swap.last_update_time_str = (!swap.last_update_time) ? '' : common.convertTimestampToDate(Math.round(swap.last_update_time/1000000000));
});
body.swaps = common.sortDescByKey(body.swaps, 'initiation_time');
logger.info({fileName: 'Loop', msg: 'Loop Swaps after Sort: ' + JSON.stringify(body)});
}
res.status(200).json(body.swaps);
})
.catch((err) => {
return res.status(500).json({
message: "Loop Swaps Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};
exports.swap = (req, res, next) => {
swapServerUrl = common.getSelSwapServerUrl();
if(swapServerUrl === '') { return res.status(500).json({message: "Loop Out Failed!",error: { message: 'Loop Server URL is missing in the configuration.'}}); }
options.url = swapServerUrl + '/loop/swap/' + req.params.id;
request(options).then(function (body) {
logger.info({fileName: 'Loop', msg: 'Loop Swap: ' + body});
body = JSON.parse(body);
body.initiation_time_str = (!body.initiation_time) ? '' : common.convertTimestampToDate(Math.round(body.initiation_time/1000000000));
body.last_update_time_str = (!body.last_update_time) ? '' : common.convertTimestampToDate(Math.round(body.last_update_time/1000000000));
res.status(200).json(body);
})
.catch((err) => {
return res.status(500).json({
message: "Loop Swap Failed!",
error: err.error.error ? err.error.error : err.error
});
});
};

@ -42,6 +42,7 @@ parameters have `default` values for initial setup and can be updated after RTL
PORT (port number for the rtl node server, default 3000, Required)
LN_IMPLEMENTATION (LND, CLT. Default 'LND', Required)
LN_SERVER_URL (LND server URL for REST APIs, default https://localhost:8080/v1) OR LN_SERVER_URL (LN server URL for LNP REST APIs) (Required)
SWAP_SERVER_URL (Swap server URL for REST APIs, default http://localhost:8081/v1) (Optional)
CONFIG_PATH (Full path of the lnd.conf file including the file name) OR CONFIG_PATH (Full path of the LNP .conf file including the file name) (Optional)
MACAROON_PATH (Path for the folder containing 'admin.macaroon' file, Required)
RTL_SSO (1 - single sign on via an external cookie, 0 - stand alone RTL authentication, Optional)

2
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "rtl",
"version": "0.6.8-beta",
"version": "0.7.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "rtl",
"version": "0.6.8-beta",
"version": "0.7.0-beta",
"license": "MIT",
"scripts": {
"ng": "ng",

@ -0,0 +1,17 @@
const LoopController = require("../../controllers/lnd/loop");
const express = require("express");
const router = express.Router();
const authCheck = require("../authCheck");
router.get("/in/terms", authCheck, LoopController.loopInTerms);
router.get("/in/quote/:amount", authCheck, LoopController.loopInQuote);
router.get("/in/termsAndQuotes", authCheck, LoopController.loopInTermsAndQuotes);
router.post("/in", authCheck, LoopController.loopIn);
router.get("/out/terms", authCheck, LoopController.loopOutTerms);
router.get("/out/quote/:amount", authCheck, LoopController.loopOutQuote);
router.get("/out/termsAndQuotes", authCheck, LoopController.loopOutTermsAndQuotes);
router.post("/out", authCheck, LoopController.loopOut);
router.get("/swaps", authCheck, LoopController.swaps);
router.get("/swap/:id", authCheck, LoopController.swap);
module.exports = router;

@ -18,6 +18,7 @@ import { AppComponent } from './app.component';
import { environment } from '../environments/environment';
import { SessionService } from './shared/services/session.service';
import { CommonService } from './shared/services/common.service';
import { LoopService } from './shared/services/loop.service';
import { DataService } from './shared/services/data.service';
import { LoggerService, ConsoleLoggerService } from './shared/services/logger.service';
import { AuthGuard } from './shared/services/auth.guard';
@ -40,6 +41,7 @@ import { SpinnerDialogComponent } from './shared/components/data-modal/spinner-d
import { AlertMessageComponent } from './shared/components/data-modal/alert-message/alert-message.component';
import { ConfirmationMessageComponent } from './shared/components/data-modal/confirmation-message/confirmation-message.component';
import { ErrorMessageComponent } from './shared/components/data-modal/error-message/error-message.component';
import { LoopModalComponent } from './lnd/loop/loop-modal/loop-modal.component';
@NgModule({
imports: [
@ -67,7 +69,8 @@ import { ErrorMessageComponent } from './shared/components/data-modal/error-mess
AlertMessageComponent,
ConfirmationMessageComponent,
ErrorMessageComponent,
CloseChannelComponent
CloseChannelComponent,
LoopModalComponent
],
entryComponents: [
CLInvoiceInformationComponent,
@ -81,7 +84,8 @@ import { ErrorMessageComponent } from './shared/components/data-modal/error-mess
AlertMessageComponent,
ConfirmationMessageComponent,
ErrorMessageComponent,
CloseChannelComponent
CloseChannelComponent,
LoopModalComponent
],
providers: [
{ provide: LoggerService, useClass: ConsoleLoggerService },
@ -89,7 +93,7 @@ import { ErrorMessageComponent } from './shared/components/data-modal/error-mess
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { hasBackdrop: true, autoFocus: true, disableClose: true, role: 'dialog', width: '55%' } },
CommonService, AuthGuard, SessionService, DataService
CommonService, AuthGuard, SessionService, DataService, LoopService
],
bootstrap: [AppComponent]
})

@ -11,7 +11,8 @@
<span class="dashboard-capacity-header" matTooltip="{{channel.remote_alias || channel.remote_pubkey}}" matTooltipDisabled="{{(channel.remote_alias || channel.remote_pubkey).length < 26}}">{{(channel.remote_alias || channel.remote_pubkey) | slice:0:24}}{{(channel.remote_alias || channel.remote_pubkey).length > 25 ? '...' : ''}}</span>
<div fxLayout="row" fxLayoutAlign="space-between stretch" class="w-100">
<mat-hint *ngIf="direction === 'In'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.remote_balance || 0 | number}} Sats</mat-hint>
<mat-hint fxFlex="100" *ngIf="direction === 'Out'" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.local_balance || 0 | number}} Sats</mat-hint>
<mat-hint [fxFlex]="showLoop ? '85' : '100'" *ngIf="direction === 'Out'" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.local_balance || 0 | number}} Sats</mat-hint>
<button *ngIf="showLoop && direction === 'Out'" fxLayout="column" fxLayoutAlign="center end" class="button-link-dashboard" color="primary" fxFlex="15" mat-button aria-label="Loop Out" (click)="onLoopOut(channel)">Loop Out</button>
</div>
<mat-progress-bar *ngIf="direction === 'In'" fxFlex="100" class="dashboard-progress-bar" mode="determinate" value="{{(totalLiquidity > 0) ? ((+channel.remote_balance || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar>
<mat-progress-bar *ngIf="direction === 'Out'" fxFlex="100" class="dashboard-progress-bar" mode="determinate" value="{{(totalLiquidity > 0) ? ((+channel.local_balance || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar>

@ -1,11 +1,16 @@
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { SwapTypeEnum } from '../../../shared/services/consts-enums-functions';
import { Channel } from '../../../shared/models/lndModels';
import { LoopModalComponent } from '../../loop/loop-modal/loop-modal.component';
import { LoopService } from '../../../shared/services/loop.service';
import * as fromRTLReducer from '../../../store/rtl.reducers';
import * as RTLActions from '../../../store/rtl.actions';
@Component({
selector: 'rtl-channel-liquidity-info',
@ -16,17 +21,40 @@ export class ChannelLiquidityInfoComponent implements OnInit, OnDestroy {
@Input() direction: string;
@Input() totalLiquidity: number;
@Input() allChannels: Channel[];
public showLoop: boolean;
private targetConf = 6;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private router: Router, private store: Store<fromRTLReducer.RTLState>) {}
constructor(private router: Router, private loopService: LoopService, private store: Store<fromRTLReducer.RTLState>) {}
ngOnInit() {}
ngOnInit() {
this.store.select('lnd')
.pipe(takeUntil(this.unSubs[0]))
.subscribe((rtlStore) => {
this.showLoop = (rtlStore.nodeSettings.swapServerUrl && rtlStore.nodeSettings.swapServerUrl.trim() !== '') ? true : false;
});
}
goToChannels() {
this.router.navigateByUrl('/lnd/peerschannels');
}
onLoopOut(channel: Channel) {
this.store.dispatch(new RTLActions.OpenSpinner('Getting Terms and Quotes...'));
this.loopService.getLoopOutTermsAndQuotes(this.targetConf)
.pipe(takeUntil(this.unSubs[1]))
.subscribe(response => {
this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new RTLActions.OpenAlert({ minHeight: '56rem', data: {
channel: channel,
minQuote: response[0],
maxQuote: response[1],
direction: SwapTypeEnum.LOOP_OUT,
component: LoopModalComponent
}}));
});
}
ngOnDestroy() {
this.unSubs.forEach(completeSub => {
completeSub.next();

@ -35,6 +35,7 @@ export class HomeComponent implements OnInit, OnDestroy {
public inactiveChannels = 0;
public channelBalances = {localBalance: 0, remoteBalance: 0, balancedness: '0'};
public selNode: SelNodeChild = {};
public showLoop = false;
public fees: Fees;
public information: GetInfo = {};
public balances = { onchain: -1, lightning: -1, total: 0 };
@ -127,6 +128,7 @@ export class HomeComponent implements OnInit, OnDestroy {
}
});
this.selNode = rtlStore.nodeSettings;
this.showLoop = (this.selNode.swapServerUrl && this.selNode.swapServerUrl.trim() !== '') ? true : false;
this.information = rtlStore.information;
if (this.flgLoading[0] !== 'error') {
this.flgLoading[0] = ( this.information.identity_pubkey) ? false : true;

@ -43,6 +43,8 @@ import { ChannelStatusInfoComponent } from './home/channel-status-info/channel-s
import { ChannelCapacityInfoComponent } from './home/channel-capacity-info/channel-capacity-info.component';
import { ChannelLiquidityInfoComponent } from './home/channel-liquidity-info/channel-liquidity-info.component';
import { NetworkInfoComponent } from './network-info/network-info.component';
import { LoopComponent } from './loop/loop.component';
import { SwapsComponent } from './loop/swaps/swaps.component';
import { LNDUnlockedGuard } from '../shared/services/auth.guard';
@ -91,7 +93,9 @@ import { LNDUnlockedGuard } from '../shared/services/auth.guard';
ChannelStatusInfoComponent,
ChannelCapacityInfoComponent,
ChannelLiquidityInfoComponent,
NetworkInfoComponent
NetworkInfoComponent,
LoopComponent,
SwapsComponent
],
providers: [
LNDUnlockedGuard

@ -10,6 +10,7 @@ import { LookupsComponent } from './lookups/lookups.component';
import { RoutingComponent } from './routing/routing.component';
import { OnChainComponent } from './on-chain/on-chain.component';
import { NetworkInfoComponent } from './network-info/network-info.component';
import { LoopComponent } from './loop/loop.component';
import { BackupComponent } from './backup/backup.component';
import { SignVerifyMessageComponent } from './sign-verify-message/sign-verify-message.component';
import { NotFoundComponent } from '../shared/components/not-found/not-found.component';
@ -29,6 +30,7 @@ export const LndRoutes: Routes = [
{ path: 'routing', component: RoutingComponent, canActivate: [LNDUnlockedGuard] },
{ path: 'lookups', component: LookupsComponent, canActivate: [LNDUnlockedGuard] },
{ path: 'network', component: NetworkInfoComponent, canActivate: [LNDUnlockedGuard] },
{ path: 'loop', component: LoopComponent, canActivate: [LNDUnlockedGuard] },
{ path: '**', component: NotFoundComponent },
{ path: 'rates', redirectTo: 'network' }
]}

File diff suppressed because one or more lines are too long

@ -0,0 +1,5 @@
svg {
height: 50%;
min-height: 50%;
max-height: 50%;
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoopInInfoGraphicsComponent } from './info-graphics.component';
describe('LoopInInfoGraphicsComponent', () => {
let component: LoopInInfoGraphicsComponent;
let fixture: ComponentFixture<LoopInInfoGraphicsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoopInInfoGraphicsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoopInInfoGraphicsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,34 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { ScreenSizeEnum } from '../../../shared/services/consts-enums-functions';
import { sliderAnimation } from '../../../shared/animation/slider-animation';
@Component({
selector: 'rtl-loop-in-info-graphics',
templateUrl: './info-graphics.component.html',
styleUrls: ['./info-graphics.component.scss'],
animations: [sliderAnimation]
})
export class LoopInInfoGraphicsComponent implements OnInit {
@Input() animationDirection = 'forward';
@Input() stepNumber = 1;
@Output() stepNumberChange = new EventEmitter();
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
constructor() {}
ngOnInit() {}
onSwipe(event: any) {
if(event.direction === 2 && this.stepNumber < 5) {
this.stepNumber++;
this.animationDirection = 'forward';
this.stepNumberChange.emit(this.stepNumber);
} else if(event.direction === 4 && this.stepNumber > 1) {
this.stepNumber--;
this.animationDirection = 'backward';
this.stepNumberChange.emit(this.stepNumber);
}
}
}

@ -0,0 +1,136 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch" *ngIf="!flgShowInfo" [@opacityAnimation]>
<div fxFlex="100" class="padding-gap-large">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between center" class="modal-info-header">
<div [fxFlex]="screenSize === screenSizeEnum.XS || screenSize === screenSizeEnum.SM ? '83' : '91'" fxLayoutAlign="start start" class="padding-gap-x-large"><span class="page-title">{{channel ? ('Channel ' + loopDirectionCaption) : loopDirectionCaption}}</span></div>
<div [fxFlex]="screenSize === screenSizeEnum.XS || screenSize === screenSizeEnum.SM ? '17' : '9'" fxLayoutAlign="space-between end">
<button tabindex="21" class="btn-close-x p-0" (click)="showInfo()" mat-button>?</button>
<button tabindex="22" class="btn-close-x p-0" (click)="onClose()" mat-button>X</button>
</div>
</mat-card-header>
<mat-card-content class="mt-5px">
<div fxLayout="column">
<div *ngIf="channel" class="padding-gap-large" fxLayout="row wrap" fxLayoutAlign="space-between stretch">
<p fxFlex="40"><strong>Channel Peer:&nbsp;</strong>{{channel.remote_alias | titlecase}}</p>
<p fxFlex="30"><strong>Channel ID:&nbsp;</strong>{{channel.chan_id}}</p>
<p fxFlex="30"></p>
</div>
<mat-vertical-stepper [linear]="true" #stepper (selectionChange)="stepSelectionChanged($event)">
<mat-step [stepControl]="inputFormGroup" [editable]="flgEditable">
<form [formGroup]="inputFormGroup" fxLayout="column" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
<ng-template matStepLabel>{{inputFormLabel}}</ng-template>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch">
<rtl-loop-quote [quote]="minQuote" [termCaption]="'min'" [panelExpanded]="false" [showPanel]="true"></rtl-loop-quote>
<rtl-loop-quote [quote]="maxQuote" [termCaption]="'max'" [panelExpanded]="false" [showPanel]="true"></rtl-loop-quote>
</div>
<div fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between center" class="mt-1">
<mat-form-field [fxFlex]="direction === swapTypeEnum.LOOP_OUT ? '30' : '48'">
<input autoFocus matInput placeholder="Amount" type="number" step="1000" tabindex="1" formControlName="amount" required>
<mat-hint>Range: {{minQuote.amount | number}}-{{maxQuote.amount | number}}</mat-hint>
<span matSuffix>Sats</span>
<mat-error *ngIf="inputFormGroup.controls.amount.errors?.required">Amount is required.</mat-error>
<mat-error *ngIf="inputFormGroup.controls.amount.errors?.min">Amount must be greater than or equal to {{minQuote.amount | number}}.</mat-error>
<mat-error *ngIf="inputFormGroup.controls.amount.errors?.max">Amount must be less than or equal to {{maxQuote.amount | number}}.</mat-error>
</mat-form-field>
<mat-form-field [fxFlex]="direction === swapTypeEnum.LOOP_OUT ? '20' : '48'">
<input matInput placeholder="Sweep Confirmation Target" type="number" step="1" tabindex="2" formControlName="sweepConfTarget" required>
<mat-error *ngIf="inputFormGroup.controls.sweepConfTarget.errors?.required">Confirmation target is required.</mat-error>
<mat-error *ngIf="inputFormGroup.controls.sweepConfTarget.errors?.min">Confirmation target must be a positive number.</mat-error>
</mat-form-field>
<mat-form-field *ngIf="direction === swapTypeEnum.LOOP_OUT" fxFlex="30">
<input matInput placeholder="Max Off-chain Routing Fee (%)" type="number" step="1" tabindex="3" formControlName="routingFeePercent" required>
<mat-error *ngIf="inputFormGroup.controls.routingFeePercent.errors?.required">Percentage is required.</mat-error>
<mat-error *ngIf="inputFormGroup.controls.routingFeePercent.errors?.min">Percentage must be a positive number.</mat-error>
<mat-error *ngIf="inputFormGroup.controls.routingFeePercent.errors?.max">Percentage must be less than or equal to {{maxRoutingFeePercentage}}.</mat-error>
</mat-form-field>
<mat-slide-toggle *ngIf="direction === swapTypeEnum.LOOP_OUT" fxFlex="15" tabindex="4" color="primary" formControlName="fast">Fast</mat-slide-toggle>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button mat-stroked-button color="primary" tabindex="5" type="button" (click)="onEstimateQuote()">Estimate Quote</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="quoteFormGroup" [editable]="flgEditable">
<form [formGroup]="quoteFormGroup" fxLayout="column" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
<ng-template matStepLabel>{{quoteFormLabel}}</ng-template>
<rtl-loop-quote [quote]="quote" [showPanel]="false"></rtl-loop-quote>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button *ngIf="direction === swapTypeEnum.LOOP_OUT" mat-stroked-button color="primary" tabindex="6" type="button" matStepperNext>Next</button>
<button *ngIf="direction === swapTypeEnum.LOOP_IN" mat-stroked-button color="primary" tabindex="7" type="button" (click)="onLoop()">Initiate {{loopDirectionCaption}}</button>
</div>
</form>
</mat-step>
<mat-step *ngIf="direction === swapTypeEnum.LOOP_OUT" [stepControl]="addressFormGroup" [editable]="flgEditable">
<form [formGroup]="addressFormGroup" fxLayout="column" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
<ng-template matStepLabel>{{addressFormLabel}}</ng-template>
<div fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch" class="mt-1">
<mat-radio-group color="primary" name="addressType" (change)="onAddressTypeChange($event)" formControlName="addressType" fxFlex="100" fxLayoutAlign="space-between stretch">
<mat-radio-button fxFlex="48" tabindex="8" value="local">Node Local Address</mat-radio-button>
<mat-radio-button fxFlex="48" tabindex="9" value="external">External Address</mat-radio-button>
</mat-radio-group>
<mat-form-field fxFlex="100" class="mt-1">
<input matInput placeholder="Address" tabindex="10" formControlName="address" [required]="addressFormGroup.controls.addressType.value === 'external'">
<mat-error *ngIf="addressFormGroup.controls.address.errors?.required">Address is required.</mat-error>
</mat-form-field>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button mat-stroked-button color="primary" tabindex="11" type="button" (click)="onLoop()">Initiate {{loopDirectionCaption}}</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="statusFormGroup">
<form [formGroup]="statusFormGroup" fxLayout="column" fxLayoutAlign="start" fxLayoutAlign.gt-sm="space-between" class="my-1">
<ng-template matStepLabel>{{loopDirectionCaption}} Status</ng-template>
<div fxLayout="row wrap" fxFlex="100" fxLayoutAlign="space-between stretch">
<mat-expansion-panel class="flat-expansion-panel" fxFlex="100" [expanded]="loopStatus">
<mat-expansion-panel-header>
<mat-panel-title>
<span fxLayoutAlign="start center" fxFlex="100">{{(!loopStatus) ? ('Waiting for ' + loopDirectionCaption + ' request...') : (loopStatus.id_bytes) ? (loopDirectionCaption + ' request details') : (loopDirectionCaption + ' error details')}}<mat-icon *ngIf="loopStatus" class="ml-1 icon-small">{{(loopStatus && loopStatus?.id_bytes) ? 'check' : 'close'}}</mat-icon></span>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="!loopStatus; else loopStatusBlock"></div>
</mat-expansion-panel>
<mat-progress-bar fxFlex="100" *ngIf="!loopStatus" color="primary" mode="indeterminate"></mat-progress-bar>
</div>
<h4 *ngIf="loopStatus" fxLayoutAlign="start" class="font-bold-500 mt-2">{{(loopStatus && loopStatus.error) ? (loopDirectionCaption + ' failed.') : (loopStatus && loopStatus.id_bytes && channel) ? (loopDirectionCaption + ' request placed successfully. Go to loop to check it\'s status.') : (loopDirectionCaption + ' request placed successfully.')}}</h4>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button *ngIf="loopStatus && loopStatus.id_bytes && channel" mat-flat-button color="primary" tabindex="12" type="button" (click)="goToLoop()">Check Status</button>
<button *ngIf="loopStatus && (loopStatus.error || !loopStatus.id_bytes)" mat-flat-button color="primary" tabindex="13" type="button" (click)="stepper.reset()">Start Again</button>
</div>
</form>
</mat-step>
</mat-vertical-stepper>
<div fxLayout="row" fxFlex="100" fxLayoutAlign="end end">
<button mat-stroked-button color="primary" tabindex="14" type="button" [mat-dialog-close]="false" default>Close</button>
</div>
</div>
</mat-card-content>
</div>
</div>
<ng-template #loopStatusBlock>
<rtl-loop-status fxLayout="column" [loopStatus]="loopStatus"></rtl-loop-status>
</ng-template>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch" *ngIf="flgShowInfo" [@opacityAnimation] class="info-graphics-container">
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch" class="padding-gap-large">
<mat-card-header fxLayout="row" fxFlex="5" fxLayoutAlign="space-between center" class="modal-info-header">
<div fxFlex="95" fxLayoutAlign="start start" class="padding-gap-x-large"><span class="page-title"></span></div>
<div fxFlex="8" fxLayoutAlign="end center">
<button tabindex="19" class="btn-close-x p-0" (click)="flgShowInfo=false;stepNumber=1;" mat-button>X</button>
</div>
</mat-card-header>
<mat-card-content fxLayout="column" fxFlex="70" fxLayoutAlign="space-between center">
<rtl-loop-out-info-graphics *ngIf="direction === swapTypeEnum.LOOP_OUT" [(stepNumber)]="stepNumber" [animationDirection]="animationDirection"></rtl-loop-out-info-graphics>
<rtl-loop-in-info-graphics *ngIf="direction === swapTypeEnum.LOOP_IN" [(stepNumber)]="stepNumber" [animationDirection]="animationDirection"></rtl-loop-in-info-graphics>
</mat-card-content>
<div class="my-4" fxLayout="row" fxFlex="10" fxLayoutAlign="center end">
<span *ngFor="let i of [1, 2, 3, 4, 5];" (click) = "onStepChanged(i)" fxLayoutAlign="center center" class="dots-stepper-block">
<p class="dot tiny-dot mr-0" [ngClass]="{'dot-primary': stepNumber === i, 'dot-primary-lighter': stepNumber !== i}"></p>
</span>
</div>
<div fxLayout="row" fxFlex="15" fxLayoutAlign="end end" class="mt-2">
<button *ngIf="stepNumber === 5" mat-stroked-button class="mr-1" color="primary" tabindex="15" type="button" (click)="onReadMore()">Read More</button>
<button *ngIf="stepNumber === 5" mat-flat-button color="primary" tabindex="16" type="button" (click)="flgShowInfo=false;stepNumber=1;">Close</button>
<button *ngIf="stepNumber < 5" mat-stroked-button class="mr-1" color="primary" tabindex="17" type="button" (click)="flgShowInfo=false;stepNumber=1;">Close</button>
<button *ngIf="stepNumber < 5" mat-flat-button color="primary" tabindex="18" type="button" (click)="onStepChanged(stepNumber + 1)">Next</button>
</div>
</div>
</div>

@ -0,0 +1,3 @@
.dots-stepper-block {
width: 3rem;
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoopModalComponent } from './loop-modal.component';
describe('LoopModalComponent', () => {
let component: LoopModalComponent;
let fixture: ComponentFixture<LoopModalComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoopModalComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoopModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,263 @@
import { Component, OnInit, Inject, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DecimalPipe } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { MatDialogRef, MAT_DIALOG_DATA, MatVerticalStepper } from '@angular/material';
import { Store } from '@ngrx/store';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { opacityAnimation } from '../../../shared/animation/opacity-animation';
import { ScreenSizeEnum, SwapTypeEnum } from '../../../shared/services/consts-enums-functions';
import { LoopQuote, LoopStatus } from '../../../shared/models/loopModels';
import { LoopAlert } from '../../../shared/models/alertData';
import { LoopService } from '../../../shared/services/loop.service';
import { LoggerService } from '../../../shared/services/logger.service';
import { CommonService } from '../../../shared/services/common.service';
import { Channel } from '../../../shared/models/lndModels';
import * as RTLActions from '../../../store/rtl.actions';
import * as fromRTLReducer from '../../../store/rtl.reducers';
@Component({
selector: 'rtl-loop-modal',
templateUrl: './loop-modal.component.html',
styleUrls: ['./loop-modal.component.scss'],
animations: [opacityAnimation]
})
export class LoopModalComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('stepper', { static: false }) stepper: MatVerticalStepper;
public faInfoCircle = faInfoCircle;
public quote: LoopQuote;
public channel: Channel;
public minQuote: LoopQuote;
public maxQuote: LoopQuote;
public swapTypeEnum = SwapTypeEnum;
public direction = SwapTypeEnum.LOOP_OUT;
public loopDirectionCaption = 'Loop out';
public loopStatus: LoopStatus = null;
public inputFormLabel = 'Amount to loop out';
public quoteFormLabel = 'Confirm Quote';
public addressFormLabel = 'Withdrawal Address';
public maxRoutingFeePercentage = 2;
public prepayRoutingFee = 36;
public flgShowInfo = false;
public stepNumber = 1;
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
public animationDirection = 'forward';
public flgEditable = true;
inputFormGroup: FormGroup;
quoteFormGroup: FormGroup;
addressFormGroup: FormGroup;
statusFormGroup: FormGroup;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<LoopModalComponent>, @Inject(MAT_DIALOG_DATA) public data: LoopAlert, private store: Store<fromRTLReducer.RTLState>, private loopService: LoopService, private formBuilder: FormBuilder, private decimalPipe: DecimalPipe, private logger: LoggerService, private router: Router, private commonService: CommonService) { }
ngOnInit() {
this.screenSize = this.commonService.getScreenSize();
this.channel = this.data.channel;
this.minQuote = this.data.minQuote ? this.data.minQuote : {};
this.maxQuote = this.data.maxQuote ? this.data.maxQuote : {};
this.direction = this.data.direction;
this.loopDirectionCaption = this.direction === SwapTypeEnum.LOOP_IN ? 'Loop in' : 'Loop out';
this.inputFormLabel = 'Amount to ' + this.loopDirectionCaption;
this.inputFormGroup = this.formBuilder.group({
amount: [this.minQuote.amount, [Validators.required, Validators.min(this.minQuote.amount), Validators.max(this.maxQuote.amount)]],
sweepConfTarget: [6, [Validators.required, Validators.min(1)]],
routingFeePercent: [this.maxRoutingFeePercentage, [Validators.required, Validators.min(0), Validators.max(this.maxRoutingFeePercentage)]],
fast: [false, [Validators.required]]
});
this.quoteFormGroup = this.formBuilder.group({});
this.addressFormGroup = this.formBuilder.group({
addressType: ['local', [Validators.required]],
address: [{value: '', disabled: true}]
});
this.statusFormGroup = this.formBuilder.group({});
this.onFormValueChanges();
}
ngAfterViewInit() {
this.inputFormGroup.setErrors({'Invalid': true});
if (this.direction === SwapTypeEnum.LOOP_OUT) {
this.addressFormGroup.setErrors({'Invalid': true});
}
}
onFormValueChanges() {
this.inputFormGroup.valueChanges.pipe(takeUntil(this.unSubs[4])).subscribe(changedValues => {
this.inputFormGroup.setErrors({'Invalid': true});
});
if (this.direction === SwapTypeEnum.LOOP_OUT) {
this.addressFormGroup.valueChanges.pipe(takeUntil(this.unSubs[5])).subscribe(changedValues => {
this.addressFormGroup.setErrors({'Invalid': true});
});
}
}
onAddressTypeChange(event: any) {
if (event.value === 'external') {
this.addressFormGroup.controls.address.setValidators([Validators.required]);
this.addressFormGroup.controls.address.markAsTouched();
this.addressFormGroup.controls.address.enable();
} else {
this.addressFormGroup.controls.address.setValidators(null);
this.addressFormGroup.controls.address.markAsPristine();
this.addressFormGroup.controls.address.disable();
this.addressFormGroup.controls.address.setValue('');
}
this.addressFormGroup.setErrors({'Invalid': true});
}
onLoop() {
if(!this.inputFormGroup.controls.amount.value || this.inputFormGroup.controls.amount.value < this.minQuote.amount || this.inputFormGroup.controls.amount.value > this.maxQuote.amount || !this.inputFormGroup.controls.sweepConfTarget.value || this.inputFormGroup.controls.sweepConfTarget.value < 2 || (!this.inputFormGroup.controls.routingFeePercent.value || this.inputFormGroup.controls.routingFeePercent.value < 0 || this.inputFormGroup.controls.routingFeePercent.value > this.maxRoutingFeePercentage) || (this.direction === SwapTypeEnum.LOOP_OUT && this.addressFormGroup.controls.addressType.value === 'external' && (!this.addressFormGroup.controls.address.value || this.addressFormGroup.controls.address.value.trim() === ''))) { return true; }
this.flgEditable = false;
this.stepper.selected.stepControl.setErrors(null);
this.stepper.next();
if (this.direction === SwapTypeEnum.LOOP_IN) {
this.loopService.loopIn(this.inputFormGroup.controls.amount.value, +this.quote.swap_fee, +this.quote.miner_fee, '', true).pipe(takeUntil(this.unSubs[0]))
.subscribe((loopStatus: any) => {
this.loopStatus = JSON.parse(loopStatus);
this.store.dispatch(new RTLActions.FetchLoopSwaps());
this.flgEditable = true;
}, (err) => {
this.loopStatus = { error: err.error.error ? err.error.error : err.error ? err.error : err };
this.flgEditable = true;
this.logger.error(err);
});
} else {
let swapRoutingFee = this.inputFormGroup.controls.amount.value * (this.inputFormGroup.controls.routingFeePercent.value / 100);
let destAddress = this.addressFormGroup.controls.addressType.value === 'external' ? this.addressFormGroup.controls.address.value : '';
let swapPublicationDeadline = this.inputFormGroup.controls.fast.value ? 0 : new Date().getTime() + (30 * 60000);
this.loopService.loopOut(this.inputFormGroup.controls.amount.value, (this.channel && this.channel.chan_id ? this.channel.chan_id : ''), this.inputFormGroup.controls.sweepConfTarget.value, swapRoutingFee, +this.quote.miner_fee, this.prepayRoutingFee, +this.quote.prepay_amt, +this.quote.swap_fee, swapPublicationDeadline, destAddress).pipe(takeUntil(this.unSubs[1]))
.subscribe((loopStatus: any) => {
this.loopStatus = JSON.parse(loopStatus);
this.store.dispatch(new RTLActions.FetchLoopSwaps());
this.flgEditable = true;
}, (err) => {
this.loopStatus = { error: err.error.error ? err.error.error : err.error ? err.error : err };
this.flgEditable = true;
this.logger.error(err);
});
}
}
onEstimateQuote() {
if(!this.inputFormGroup.controls.amount.value || this.inputFormGroup.controls.amount.value < this.minQuote.amount || this.inputFormGroup.controls.amount.value > this.maxQuote.amount || !this.inputFormGroup.controls.sweepConfTarget.value || this.inputFormGroup.controls.sweepConfTarget.value < 2) { return true; }
this.stepper.selected.stepControl.setErrors(null);
this.stepper.next();
this.store.dispatch(new RTLActions.OpenSpinner('Getting Quotes...'));
let swapPublicationDeadline = this.inputFormGroup.controls.fast.value ? 0 : new Date().getTime() + (30 * 60000);
if(this.direction === SwapTypeEnum.LOOP_IN) {
this.loopService.getLoopInQuote(this.inputFormGroup.controls.amount.value, this.inputFormGroup.controls.sweepConfTarget.value, swapPublicationDeadline)
.pipe(takeUntil(this.unSubs[2]))
.subscribe(response => {
this.store.dispatch(new RTLActions.CloseSpinner());
this.quote = response;
this.quote.off_chain_swap_routing_fee_percentage = this.inputFormGroup.controls.routingFeePercent.value ? this.inputFormGroup.controls.routingFeePercent.value : 2;
});
} else {
this.loopService.getLoopOutQuote(this.inputFormGroup.controls.amount.value, this.inputFormGroup.controls.sweepConfTarget.value, swapPublicationDeadline)
.pipe(takeUntil(this.unSubs[3]))
.subscribe(response => {
this.store.dispatch(new RTLActions.CloseSpinner());
this.quote = response;
this.quote.off_chain_swap_routing_fee_percentage = this.inputFormGroup.controls.routingFeePercent.value ? this.inputFormGroup.controls.routingFeePercent.value : 2;
});
}
}
stepSelectionChanged(event: any) {
switch (event.selectedIndex) {
case 0:
this.inputFormLabel = 'Amount to ' + this.loopDirectionCaption;
this.quoteFormLabel = 'Confirm Quote';
this.addressFormLabel = 'Withdrawal Address';
break;
case 1:
if (this.inputFormGroup.controls.amount.value || this.inputFormGroup.controls.sweepConfTarget.value) {
if (this.direction === SwapTypeEnum.LOOP_IN) {
this.inputFormLabel = this.loopDirectionCaption + ' Amount: ' + (this.decimalPipe.transform(this.inputFormGroup.controls.amount.value ? this.inputFormGroup.controls.amount.value : 0)) + ' Sats | Target Confirmation: ' + (this.inputFormGroup.controls.sweepConfTarget.value ? this.inputFormGroup.controls.sweepConfTarget.value : 6);
} else {
this.inputFormLabel = this.loopDirectionCaption + ' Amount: ' + (this.decimalPipe.transform(this.inputFormGroup.controls.amount.value ? this.inputFormGroup.controls.amount.value : 0)) + ' Sats | Target Confirmation: ' + (this.inputFormGroup.controls.sweepConfTarget.value ? this.inputFormGroup.controls.sweepConfTarget.value : 6) + ' | Percentage: ' + (this.inputFormGroup.controls.routingFeePercent.value ? this.inputFormGroup.controls.routingFeePercent.value : '2') + ' | Fast: ' + (this.inputFormGroup.controls.fast.value ? 'Enabled' : 'Disabled');
}
} else {
this.inputFormLabel = 'Amount to ' + this.loopDirectionCaption;
}
this.quoteFormLabel = 'Confirm Quote';
this.addressFormLabel = 'Withdrawal Address';
break;
case 2:
if (this.inputFormGroup.controls.amount.value || this.inputFormGroup.controls.sweepConfTarget.value) {
if (this.direction === SwapTypeEnum.LOOP_IN) {
this.inputFormLabel = this.loopDirectionCaption + ' Amount: ' + (this.decimalPipe.transform(this.inputFormGroup.controls.amount.value ? this.inputFormGroup.controls.amount.value : 0)) + ' Sats | Target Confirmation: ' + (this.inputFormGroup.controls.sweepConfTarget.value ? this.inputFormGroup.controls.sweepConfTarget.value : 6);
} else {
this.inputFormLabel = this.loopDirectionCaption + ' Amount: ' + (this.decimalPipe.transform(this.inputFormGroup.controls.amount.value ? this.inputFormGroup.controls.amount.value : 0)) + ' Sats | Target Confirmation: ' + (this.inputFormGroup.controls.sweepConfTarget.value ? this.inputFormGroup.controls.sweepConfTarget.value : 6) + ' | Fast: ' + (this.inputFormGroup.controls.fast.value ? 'Enabled' : 'Disabled');
}
} else {
this.inputFormLabel = 'Amount to ' + this.loopDirectionCaption;
}
if (this.quote && this.quote.swap_fee && this.quote.miner_fee && this.quote.prepay_amt) {
this.quoteFormLabel = 'Quote confirmed | Estimated Fees: ' + this.decimalPipe.transform(+this.quote.swap_fee + +this.quote.miner_fee) + ' Sats';
} else {
this.quoteFormLabel = 'Quote confirmed';
}
if (this.addressFormGroup.controls.addressType.value) {
this.addressFormLabel = 'Withdrawal Address | Type: ' + this.addressFormGroup.controls.addressType.value;
} else {
this.addressFormLabel = 'Withdrawal Address';
}
break;
default:
this.inputFormLabel = 'Amount to ' + this.loopDirectionCaption;
this.quoteFormLabel = 'Confirm Quote';
this.addressFormLabel = 'Withdrawal Address';
break;
}
if ((this.direction === SwapTypeEnum.LOOP_OUT && event.selectedIndex !== 1 && event.selectedIndex < event.previouslySelectedIndex)
|| (this.direction === SwapTypeEnum.LOOP_IN && event.selectedIndex < event.previouslySelectedIndex)) {
event.selectedStep.stepControl.setErrors({'Invalid': true});
}
}
goToLoop() {
this.dialogRef.close(true);
this.router.navigateByUrl('/lnd/loop');
}
onClose() {
this.dialogRef.close(true);
}
showInfo() {
this.flgShowInfo = true;
}
onReadMore() {
if (this.direction === SwapTypeEnum.LOOP_IN) {
window.open('https://blog.lightning.engineering/announcement/2019/06/25/loop-in.html', '_blank');
} else {
window.open('https://blog.lightning.engineering/technical/posts/2019/04/15/loop-out-in-depth.html', '_blank');
}
this.onClose();
}
onStepChanged(index: number) {
this.animationDirection = index < this.stepNumber ? 'backward' : 'forward';
this.stepNumber = index;
}
ngOnDestroy() {
this.unSubs.forEach(completeSub => {
completeSub.next();
completeSub.complete();
});
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,5 @@
svg {
height: 50%;
min-height: 50%;
max-height: 50%;
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoopOutInfoGraphicsComponent } from './info-graphics.component';
describe('LoopOutInfoGraphicsComponent', () => {
let component: LoopOutInfoGraphicsComponent;
let fixture: ComponentFixture<LoopOutInfoGraphicsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoopOutInfoGraphicsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoopOutInfoGraphicsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,34 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { ScreenSizeEnum } from '../../../shared/services/consts-enums-functions';
import { sliderAnimation } from '../../../shared/animation/slider-animation';
@Component({
selector: 'rtl-loop-out-info-graphics',
templateUrl: './info-graphics.component.html',
styleUrls: ['./info-graphics.component.scss'],
animations: [sliderAnimation]
})
export class LoopOutInfoGraphicsComponent implements OnInit {
@Input() animationDirection = 'forward';
@Input() stepNumber = 1;
@Output() stepNumberChange = new EventEmitter();
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
constructor() {}
ngOnInit() {}
onSwipe(event: any) {
if(event.direction === 2 && this.stepNumber < 5) {
this.stepNumber++;
this.animationDirection = 'forward';
this.stepNumberChange.emit(this.stepNumber);
} else if(event.direction === 4 && this.stepNumber > 1) {
this.stepNumber--;
this.animationDirection = 'backward';
this.stepNumberChange.emit(this.stepNumber);
}
}
}

@ -0,0 +1,59 @@
<ng-container *ngTemplateOutlet="showPanel ? expansionPanelBlock : quoteDetailsBlock"></ng-container>
<ng-template #expansionPanelBlock>
<ng-container *ngTemplateOutlet="quote?.miner_fee < 0 ? errorBlock : informationBlock"></ng-container>
</ng-template>
<ng-template #informationBlock>
<mat-expansion-panel class="flat-expansion-panel mb-1" fxFlex="100" [expanded]="panelExpanded" [ngClass]="{'h-5':!flgShowPanel}">
<mat-expansion-panel-header>
<mat-panel-title>
<span fxLayoutAlign="start center" fxFlex="100">Quote for {{termCaption}} amount ({{quote.amount | number}} Sats)</span>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container *ngTemplateOutlet="quoteDetailsBlock"></ng-container>
</mat-expansion-panel>
</ng-template>
<ng-template #quoteDetailsBlock>
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch">
<div fxLayout="row">
<div fxFlex="30" matTooltip="The fee that the swap server is charging for the swap">
<h4 fxLayoutAlign="start" class="font-bold-500">Swap Fee (Sats)</h4>
<span class="foreground-secondary-text">{{quote?.swap_fee | number}}</span>
</div>
<div fxFlex="30" matTooltip="An estimate of the on-chain fee that needs to be paid to sweep the HTLC">
<h4 fxLayoutAlign="start" class="font-bold-500">Miner Fee (Sats)</h4>
<span class="foreground-secondary-text">{{quote?.miner_fee | number}}</span>
</div>
<div fxFlex="40" matTooltip="The part of the swap fee that is requested as a prepayment">
<h4 fxLayoutAlign="start" class="font-bold-500">Prepay Amount (Sats)</h4>
<span class="foreground-secondary-text">{{quote?.prepay_amt | number}}</span>
</div>
</div>
<mat-divider class="w-100 my-1"></mat-divider>
<div fxLayout="row">
<div fxFlex="50" matTooltip="Maximum off-chain fee that may be paid for routing the payment amount to the server">
<h4 fxLayoutAlign="start" class="font-bold-500">Max Off-chain Swap Routing Fee (Sats)</h4>
<span class="foreground-secondary-text">{{(quote?.amount * ((quote?.off_chain_swap_routing_fee_percentage ? quote?.off_chain_swap_routing_fee_percentage : 2) / 100)) | number}}</span>
</div>
<div fxFlex="50" matTooltip="Maximum off-chain fee that may be paid for routing the pre-payment amount to the server">
<h4 fxLayoutAlign="start" class="font-bold-500">Max Off-chain Prepay Routing Fee (Sats)</h4>
<span class="foreground-secondary-text">36</span>
</div>
</div>
<mat-divider class="w-100 my-1" *ngIf="quote?.swap_payment_dest !== ''"></mat-divider>
<div fxLayout="row" *ngIf="quote?.swap_payment_dest !== ''">
<div fxFlex="100" matTooltip="The node pubkey where the swap payment needs to be paid to">
<h4 fxLayoutAlign="start" class="font-bold-500">Swap Server Node Pubkey</h4>
<span class="foreground-secondary-text">{{quote?.swap_payment_dest}}</span>
</div>
</div>
</div>
</ng-template>
<ng-template #errorBlock>
<mat-expansion-panel class="flat-expansion-panel mb-1" fxFlex="100" [disabled]="true" [ngClass]="{'h-5':!flgShowPanel}">
<mat-expansion-panel-header>
<mat-panel-title>
<span fxLayoutAlign="start center" fxFlex="100">Quote for {{termCaption}} amount ({{quote.amount | number}} Sats): Insufficient balance to estimate quote</span>
</mat-panel-title>
</mat-expansion-panel-header>
</mat-expansion-panel>
</ng-template>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoopQuoteComponent } from './loop-quote.component';
describe('LoopQuoteComponent', () => {
let component: LoopQuoteComponent;
let fixture: ComponentFixture<LoopQuoteComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoopQuoteComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoopQuoteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,22 @@
import { Component, OnInit, Input } from '@angular/core';
import { LoopQuote } from '../../../shared/models/loopModels';
@Component({
selector: 'rtl-loop-quote',
templateUrl: './loop-quote.component.html',
styleUrls: ['./loop-quote.component.scss']
})
export class LoopQuoteComponent implements OnInit {
@Input() quote: LoopQuote = {};
@Input() termCaption = '';
@Input() showPanel = true;
@Input() panelExpanded = false;
public flgShowPanel = false;
constructor() {}
ngOnInit() {
setTimeout(() => { this.flgShowPanel = true; }, 1200);
}
}

@ -0,0 +1,23 @@
<ng-container *ngTemplateOutlet="loopStatus?.error ? loopFailedBlock : loopSuccessfulBlock"></ng-container>
<ng-template #loopFailedBlock>
<div fxLayout="column"><span
class="foreground-secondary-text">{{'Error: ' + (loopStatus?.error?.error?.error?.error ? loopStatus.error.error.error.error : loopStatus?.error?.error?.error ? loopStatus.error.error.error : loopStatus?.error?.error ? loopStatus.error.error : loopStatus?.error ? loopStatus.error : 'Unknown')}}</span>
</div>
</ng-template>
<ng-template #loopSuccessfulBlock>
<div fxLayout="column">
<div fxLayout="row">
<div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">ID</h4>
<span class="foreground-secondary-text">{{loopStatus?.id_bytes}}</span>
</div>
</div>
<mat-divider class="w-100 my-1"></mat-divider>
<div fxLayout="row">
<div fxFlex="100">
<h4 fxLayoutAlign="start" class="font-bold-500">HTLC Address</h4>
<span class="foreground-secondary-text">{{loopStatus?.htlc_address}}</span>
</div>
</div>
</div>
</ng-template>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoopStatusComponent } from './loop-status.component';
describe('LoopStatusComponent', () => {
let component: LoopStatusComponent;
let fixture: ComponentFixture<LoopStatusComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoopStatusComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoopStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,16 @@
import { Component, OnInit, Input } from '@angular/core';
import { LoopStatus } from '../../../shared/models/loopModels';
@Component({
selector: 'rtl-loop-status',
templateUrl: './loop-status.component.html',
styleUrls: ['./loop-status.component.scss']
})
export class LoopStatusComponent implements OnInit {
@Input() loopStatus: LoopStatus;
constructor() {}
ngOnInit() {}
}

@ -0,0 +1,19 @@
<div fxLayout="row" fxLayoutAlign="start center" class="padding-gap-x page-title-container">
<fa-icon [icon]="faInfinity" class="page-title-img mr-1"></fa-icon>
<span class="page-title">Loop</span>
</div>
<div fxLayout="column" class="padding-gap-x">
<mat-card>
<mat-card-content fxLayout="column">
<mat-tab-group fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch" (selectedIndexChange)="onSelectedIndexChange($event)">
<mat-tab label="Loop Out">
<button mat-flat-button color="primary" (click)="onLoop(swapTypeEnum.LOOP_OUT)" class="mt-1" type="button" tabindex="1">Start Loop Out</button>
</mat-tab>
<mat-tab label="Loop In">
<button mat-flat-button color="primary" (click)="onLoop(swapTypeEnum.LOOP_IN)" class="mt-1" type="button" tabindex="2">Start Loop In</button>
</mat-tab>
</mat-tab-group>
<rtl-swaps fxLayout="row" fxFlex="100" [selectedSwapType]="selectedSwapType"></rtl-swaps>
</mat-card-content>
</mat-card>
</div>

@ -0,0 +1,3 @@
.loop-monitor-logs {
min-height: 4rem;
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoopComponent } from './loop.component';
describe('LoopComponent', () => {
let component: LoopComponent;
let fixture: ComponentFixture<LoopComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoopComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoopComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,77 @@
import { Component, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { faInfinity } from '@fortawesome/free-solid-svg-icons';
import { SwapTypeEnum } from '../../shared/services/consts-enums-functions';
import { LoopModalComponent } from './loop-modal/loop-modal.component';
import { LoopQuote } from '../../shared/models/loopModels';
import { LoopService } from '../../shared/services/loop.service';
import * as fromRTLReducer from '../../store/rtl.reducers';
import * as RTLActions from '../../store/rtl.actions';
@Component({
selector: 'rtl-loop',
templateUrl: './loop.component.html',
styleUrls: ['./loop.component.scss']
})
export class LoopComponent implements OnInit {
public faInfinity = faInfinity;
private targetConf = 2;
public inAmount = 250000;
public quotes: LoopQuote[] = [];
public swapTypeEnum = SwapTypeEnum;
public selectedSwapType: SwapTypeEnum = SwapTypeEnum.LOOP_OUT;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private loopService: LoopService, private store: Store<fromRTLReducer.RTLState>) {}
ngOnInit() {}
onSelectedIndexChange(event: any) {
if(event === 1) {
this.selectedSwapType = SwapTypeEnum.LOOP_IN;
} else {
this.selectedSwapType = SwapTypeEnum.LOOP_OUT;
}
}
onLoop(direction: SwapTypeEnum) {
this.store.dispatch(new RTLActions.OpenSpinner('Getting Terms and Quotes...'));
if(direction === SwapTypeEnum.LOOP_IN) {
this.loopService.getLoopInTermsAndQuotes(this.targetConf)
.pipe(takeUntil(this.unSubs[0]))
.subscribe(response => {
this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new RTLActions.OpenAlert({ data: {
minQuote: response[0],
maxQuote: response[1],
direction: direction,
component: LoopModalComponent
}}));
});
} else {
this.loopService.getLoopOutTermsAndQuotes(this.targetConf)
.pipe(takeUntil(this.unSubs[1]))
.subscribe(response => {
this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new RTLActions.OpenAlert({ data: {
minQuote: response[0],
maxQuote: response[1],
direction: direction,
component: LoopModalComponent
}}));
});
}
}
ngOnDestroy() {
this.unSubs.forEach(completeSub => {
completeSub.next();
completeSub.complete();
});
}
}

@ -0,0 +1,84 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="start start" class="card-content-gap">
<div fxLayout="column" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" fxFlex="100" class="page-sub-title-container w-100">
<div fxFlex="70">
<fa-icon [icon]="faHistory" class="page-title-img mr-1"></fa-icon>
<span class="page-title">{{swapCaption}} History</span>
</div>
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutAlign="start center" class="w-100">
<div perfectScrollbar class="table-container" fxFlex="100">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="listSwaps" matSort [ngClass]="{'overflow-auto error-border': flgLoading[0]==='error','overflow-auto': true}">
<ng-container matColumnDef="initiation_time_str">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Initiation Time </th>
<td mat-cell *matCellDef="let swap">{{swap.initiation_time_str}}</td>
</ng-container>
<ng-container matColumnDef="last_update_time_str">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Last Update Time </th>
<td mat-cell *matCellDef="let swap">{{swap.last_update_time_str}}</td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let swap">{{swap.id}}</td>
</ng-container>
<ng-container matColumnDef="id_bytes">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID (Bytes) </th>
<td mat-cell *matCellDef="let swap">{{swap.id_bytes}}</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef mat-sort-header> State </th>
<td mat-cell *matCellDef="let swap">{{swapStateEnum[swap.state]}}</td>
</ng-container>
<ng-container matColumnDef="htlc_address">
<th mat-header-cell *matHeaderCellDef mat-sort-header> HTLC Address </th>
<td mat-cell *matCellDef="let swap">{{swap.htlc_address}}</td>
</ng-container>
<ng-container matColumnDef="amt">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Amount (Sats) </th>
<td mat-cell *matCellDef="let swap">
<span fxLayoutAlign="end center">{{swap.amt | number}}</span>
</td>
</ng-container>
<ng-container matColumnDef="cost_server">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Cost Server (Sats) </th>
<td mat-cell *matCellDef="let swap"><span fxLayoutAlign="end center">{{swap.cost_server | number}}</span></td>
</ng-container>
<ng-container matColumnDef="cost_offchain">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Cost Offchain (Sats) </th>
<td mat-cell *matCellDef="let swap"><span fxLayoutAlign="end center">{{swap.cost_offchain | number}}</span></td>
</ng-container>
<ng-container matColumnDef="cost_onchain">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Cost Onchain (Sats) </th>
<td mat-cell *matCellDef="let swap"><span fxLayoutAlign="end center">
{{swap?.cost_onchain | number}} </span></td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="px-3">
<div class="bordered-box table-actions-select">
<mat-select placeholder="Actions" tabindex="1" class="mr-0">
<mat-select-trigger></mat-select-trigger>
<mat-option (click)="onDownloadCSV()">Download CSV</mat-option>
</mat-select>
</div>
</th>
<td mat-cell *matCellDef="let swap" class="pl-3" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" type="button" tabindex="4"
(click)="onSwapClick(swap, $event)">View Info</button>
</td>
</ng-container>
<ng-container matColumnDef="no_swap">
<td mat-footer-cell *matFooterCellDef colspan="4">
<p *ngIf="!listSwaps.data || listSwaps.data.length<1">No {{swapCaption | lowercase}} swaps available.</p>
</td>
</ng-container>
<tr mat-footer-row *matFooterRowDef="['no_swap']" [ngClass]="{'display-none': listSwaps.data && listSwaps.data.length>0}"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" [showFirstLastButtons]="screenSize === screenSizeEnum.XS ? false : true" class="mb-4"></mat-paginator>
</div>
</div>
</div>

@ -0,0 +1,4 @@
.mat-column-actions {
min-height: 4.8rem;
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SwapsComponent } from './swaps.component';
describe('SwapsComponent', () => {
let component: SwapsComponent;
let fixture: ComponentFixture<SwapsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SwapsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SwapsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,140 @@
import { Component, OnInit, OnChanges, OnDestroy, ViewChild, Input } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
import { faHistory } from '@fortawesome/free-solid-svg-icons';
import { MatTableDataSource, MatSort, MatPaginator, MatPaginatorIntl } from '@angular/material';
import { SwapStatus } from '../../../shared/models/lndModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, SwapTypeEnum, SwapStateEnum } from '../../../shared/services/consts-enums-functions';
import { LoggerService } from '../../../shared/services/logger.service';
import { CommonService } from '../../../shared/services/common.service';
import { LoopService } from '../../../shared/services/loop.service';
import * as RTLActions from '../../../store/rtl.actions';
import * as fromRTLReducer from '../../../store/rtl.reducers';
@Component({
selector: 'rtl-swaps',
templateUrl: './swaps.component.html',
styleUrls: ['./swaps.component.scss'],
providers: [
{ provide: MatPaginatorIntl, useValue: getPaginatorLabel('Swaps') }
]
})
export class SwapsComponent implements OnInit, OnChanges, OnDestroy {
@Input() selectedSwapType: SwapTypeEnum = SwapTypeEnum.LOOP_OUT;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
public swapStateEnum = SwapStateEnum;
public faHistory = faHistory;
public swapCaption = 'Loop Out';
public displayedColumns = [];
public listSwaps: any;
public storedSwaps: SwapStatus[] = [];
public filteredSwaps: SwapStatus[] = [];
public flgLoading: Array<Boolean | 'error'> = [true];
public flgSticky = false;
public pageSize = PAGE_SIZE;
public pageSizeOptions = PAGE_SIZE_OPTIONS;
public screenSize = '';
public screenSizeEnum = ScreenSizeEnum;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions, private loopService: LoopService) {
this.screenSize = this.commonService.getScreenSize();
if(this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
this.displayedColumns = ['state', 'amt', 'actions'];
} else if(this.screenSize === ScreenSizeEnum.SM) {
this.flgSticky = false;
this.displayedColumns = ['state', 'amt', 'actions'];
} else if(this.screenSize === ScreenSizeEnum.MD) {
this.flgSticky = false;
this.displayedColumns = ['state', 'initiation_time_str', 'amt', 'actions'];
} else {
this.flgSticky = true;
this.displayedColumns = ['state', 'initiation_time_str', 'amt', 'cost_server', 'cost_offchain', 'cost_onchain', 'actions'];
}
}
ngOnInit() {
this.store.dispatch(new RTLActions.FetchLoopSwaps());
this.actions$.pipe(takeUntil(this.unSubs[0]), filter((action) => action.type === RTLActions.RESET_LND_STORE)).subscribe((resetLndStore: RTLActions.ResetLNDStore) => {
this.store.dispatch(new RTLActions.FetchLoopSwaps());
});
this.store.select('lnd')
.pipe(takeUntil(this.unSubs[1]))
.subscribe((rtlStore) => {
rtlStore.effectErrorsLnd.forEach(effectsErr => {
if (effectsErr.action === 'FetchSwaps') { this.flgLoading[0] = 'error'; }
});
if (rtlStore.loopSwaps) {
this.storedSwaps = rtlStore.loopSwaps;
this.filteredSwaps = this.storedSwaps.filter(swap => swap.type === this.selectedSwapType);
this.loadSwapsTable(this.filteredSwaps);
}
if (this.flgLoading[0] !== 'error') {
this.flgLoading[0] = ( rtlStore.transactions) ? false : true;
}
this.logger.info(rtlStore);
});
}
ngOnChanges() {
this.swapCaption = (this.selectedSwapType === SwapTypeEnum.LOOP_IN) ? 'Loop In' : 'Loop Out'
this.filteredSwaps = this.storedSwaps.filter(swap => swap.type === this.selectedSwapType);
this.loadSwapsTable(this.filteredSwaps);
}
applyFilter(selFilter: string) {
this.listSwaps.filter = selFilter;
}
onSwapClick(selSwap: SwapStatus, event: any) {
this.loopService.getSwap(selSwap.id_bytes.replace(/\//g, '_').replace(/\+/g, '-')).pipe(takeUntil(this.unSubs[2]))
.subscribe((fetchedSwap: SwapStatus) => {
const reorderedSwap = [
[{key: 'state', value: SwapStateEnum[fetchedSwap.state], title: 'Status', width: 50, type: DataTypeEnum.STRING},
{key: 'amt', value: fetchedSwap.amt, title: 'Amount (Sats)', width: 50, type: DataTypeEnum.NUMBER}],
[{key: 'initiation_time_str', value: fetchedSwap.initiation_time_str, title: 'Initiation Time', width: 50, type: DataTypeEnum.DATE_TIME},
{key: 'last_update_time_str', value: fetchedSwap.last_update_time_str, title: 'Last Update Time', width: 50, type: DataTypeEnum.DATE_TIME}],
[{key: 'cost_server', value: fetchedSwap.cost_server, title: 'Server Cost (Sats)', width: 33, type: DataTypeEnum.NUMBER},
{key: 'cost_offchain', value: fetchedSwap.cost_offchain, title: 'Offchain Cost (Sats)', width: 33, type: DataTypeEnum.NUMBER},
{key: 'cost_onchain', value: fetchedSwap.cost_onchain, title: 'Onchain Cost (Sats)', width: 34, type: DataTypeEnum.NUMBER}],
[{key: 'id_bytes', value: fetchedSwap.id_bytes, title: 'ID', width: 100, type: DataTypeEnum.STRING}],
[{key: 'htlc_address', value: fetchedSwap.htlc_address, title: 'HTLC Address', width: 100, type: DataTypeEnum.STRING}]
];
this.store.dispatch(new RTLActions.OpenAlert({ data: {
type: AlertTypeEnum.INFORMATION,
alertTitle: this.swapCaption + ' Status',
message: reorderedSwap,
openedBy: 'SWAP'
}}));
});
}
loadSwapsTable(swaps) {
this.listSwaps = new MatTableDataSource<SwapStatus>([...swaps]);
this.listSwaps.sort = this.sort;
this.listSwaps.paginator = this.paginator;
this.logger.info(this.listSwaps);
}
onDownloadCSV() {
if(this.listSwaps.data && this.listSwaps.data.length > 0) {
this.commonService.downloadCSV(this.listSwaps.data, (this.selectedSwapType === SwapTypeEnum.LOOP_IN) ? 'Loop in' : 'Loop out');
}
}
ngOnDestroy() {
this.unSubs.forEach(completeSub => {
completeSub.next();
completeSub.complete();
});
}
}

@ -65,6 +65,7 @@
<mat-option (click)="onViewRemotePolicy(channel)">View Remote Fee </mat-option>
<mat-option (click)="onChannelUpdate(channel)">Update Fee Policy</mat-option>
<mat-option (click)="onCircularRebalance(channel)" *ngIf="+versionsArr[0] > 0 || +versionsArr[1] >= 9">Circular Rebalance</mat-option>
<mat-option (click)="onLoopOut(channel)" *ngIf="selNode.swapServerUrl">Loop Out</mat-option>
<mat-option (click)="onChannelClose(channel)">Close Channel</mat-option>
</mat-select>
</div>

@ -8,11 +8,13 @@ import { MatTableDataSource, MatSort, MatPaginator, MatPaginatorIntl } from '@an
import { SelNodeChild } from '../../../../../shared/models/RTLconfig';
import { Channel, GetInfo } from '../../../../../shared/models/lndModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, UserPersonaEnum } from '../../../../../shared/services/consts-enums-functions';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, UserPersonaEnum, SwapTypeEnum } from '../../../../../shared/services/consts-enums-functions';
import { LoggerService } from '../../../../../shared/services/logger.service';
import { LoopService } from '../../../../../shared/services/loop.service';
import { CommonService } from '../../../../../shared/services/common.service';
import { ChannelRebalanceComponent } from '../../channel-rebalance-modal/channel-rebalance.component';
import { CloseChannelComponent } from '../../close-channel-modal/close-channel.component';
import { LoopModalComponent } from '../../../../loop/loop-modal/loop-modal.component';
import { LNDEffects } from '../../../../store/lnd.effects';
import { RTLEffects } from '../../../../../store/rtl.effects';
@ -51,7 +53,7 @@ export class ChannelOpenTableComponent implements OnInit, OnDestroy {
private targetConf = 6;
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private rtlEffects: RTLEffects, private lndEffects: LNDEffects, private commonService: CommonService, private decimalPipe: DecimalPipe) {
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private rtlEffects: RTLEffects, private lndEffects: LNDEffects, private commonService: CommonService, private loopService: LoopService, private decimalPipe: DecimalPipe) {
this.screenSize = this.commonService.getScreenSize();
if(this.screenSize === ScreenSizeEnum.XS) {
this.flgSticky = false;
@ -310,6 +312,22 @@ export class ChannelOpenTableComponent implements OnInit, OnDestroy {
return channels;
}
onLoopOut(selChannel: Channel) {
this.store.dispatch(new RTLActions.OpenSpinner('Getting Terms and Quotes...'));
this.loopService.getLoopOutTermsAndQuotes(this.targetConf)
.pipe(takeUntil(this.unSubs[0]))
.subscribe(response => {
this.store.dispatch(new RTLActions.CloseSpinner());
this.store.dispatch(new RTLActions.OpenAlert({ minHeight: '56rem', data: {
channel: selChannel,
minQuote: response[0],
maxQuote: response[1],
direction: SwapTypeEnum.LOOP_OUT,
component: LoopModalComponent
}}));
});
}
onDownloadCSV() {
if(this.channels.data && this.channels.data.length > 0) {
this.commonService.downloadCSV(this.channels.data, 'Open-channels');

@ -1104,7 +1104,29 @@ export class LNDEffects implements OnDestroy {
})
);
initializeRemainingData(info: any, landingPage: string) {
@Effect()
getLoopSwaps = this.actions$.pipe(
ofType(RTLActions.FETCH_LOOP_SWAPS),
mergeMap((action: RTLActions.FetchLoopSwaps) => {
this.store.dispatch(new RTLActions.ClearEffectErrorLnd('LoopSwaps'));
return this.httpClient.get(this.CHILD_API_URL + environment.LOOP_API + '/swaps')
.pipe(
map((swaps: any) => {
this.logger.info(swaps);
return {
type: RTLActions.SET_LOOP_SWAPS,
payload: swaps
};
}),
catchError((err: any) => {
this.handleErrorWithoutAlert('LoopSwaps', err);
return of({type: RTLActions.VOID});
})
);
}
));
initializeRemainingData(info: any, landingPage: string) {
this.sessionService.setItem('lndUnlocked', 'true');
if (info.chains) {
if (typeof info.chains[0] === 'string') {

@ -2,7 +2,7 @@ import { SelNodeChild } from '../../shared/models/RTLconfig';
import { ErrorPayload } from '../../shared/models/errorPayload';
import {
GetInfo, Peer, Fees, NetworkInfo, Balance, Channel, Payment, ListInvoices,
PendingChannels, ClosedChannel, Transaction, SwitchRes, PendingChannelsGroup
PendingChannels, ClosedChannel, Transaction, SwitchRes, PendingChannelsGroup, SwapStatus
} from '../../shared/models/lndModels';
import * as RTLActions from '../../store/rtl.actions';
import { UserPersonaEnum } from '../../shared/services/consts-enums-functions';
@ -31,11 +31,12 @@ export interface LNDState {
payments: Payment[];
invoices: ListInvoices;
forwardingHistory: SwitchRes;
loopSwaps: SwapStatus[];
}
export const initLNDState: LNDState = {
effectErrorsLnd: [],
nodeSettings: { userPersona: UserPersonaEnum.OPERATOR, fiatConversion: false, channelBackupPath: '', currencyUnits: [], selCurrencyUnit: '', lnImplementation: '' },
nodeSettings: { userPersona: UserPersonaEnum.OPERATOR, fiatConversion: false, channelBackupPath: '', currencyUnits: [], selCurrencyUnit: '', lnImplementation: '', swapServerUrl: '' },
information: {},
peers: [],
fees: {},
@ -57,6 +58,7 @@ export const initLNDState: LNDState = {
payments: [],
invoices: {invoices: []},
forwardingHistory: {},
loopSwaps: []
}
export function LNDReducer(state = initLNDState, action: RTLActions.RTLActions) {
@ -245,6 +247,11 @@ export function LNDReducer(state = initLNDState, action: RTLActions.RTLActions)
...state,
forwardingHistory: action.payload
};
case RTLActions.SET_LOOP_SWAPS:
return {
...state,
loopSwaps: action.payload
};
default:
return state;
}

@ -26,7 +26,13 @@
<ng-container *ngSwitchCase="dataTypeEnum.ARRAY"><span *ngFor="let arrayObj of obj.value" class="display-block w-100">{{arrayObj}}</span></ng-container>
<ng-container *ngSwitchCase="dataTypeEnum.NUMBER">{{obj.value | number:'1.0-3'}}</ng-container>
<ng-container *ngSwitchCase="dataTypeEnum.BOOLEAN">{{obj.value ? 'True' : 'False'}}</ng-container>
<ng-container *ngSwitchDefault>{{obj.value}}</ng-container>
<ng-container *ngSwitchDefault>
<p fxLayout="row" *ngIf="data.openedBy === 'SWAP' && obj.key === 'state'; else noStyleBlock" [ngClass]="{'failed-status': obj.value === swapStateEnum.FAILED}">
{{obj.value}}
<mat-icon *ngIf="obj.value === swapStateEnum.FAILED" fxLayoutAlign="end end" class="icon-failed-status">info</mat-icon>
</p>
<ng-template #noStyleBlock>{{obj.value}}</ng-template>
</ng-container>
</span>
</span>
<ng-template #emptyField>

@ -4,7 +4,7 @@ import { MatDialogRef, MAT_DIALOG_DATA, MatSnackBar } from '@angular/material';
import { CommonService } from '../../../services/common.service';
import { LoggerService } from '../../../services/logger.service';
import { AlertData } from '../../../models/alertData';
import { AlertTypeEnum, DataTypeEnum, ScreenSizeEnum } from '../../../services/consts-enums-functions';
import { AlertTypeEnum, DataTypeEnum, ScreenSizeEnum, SwapStateEnum } from '../../../services/consts-enums-functions';
@Component({
selector: 'rtl-alert-message',
@ -12,6 +12,7 @@ import { AlertTypeEnum, DataTypeEnum, ScreenSizeEnum } from '../../../services/c
styleUrls: ['./alert-message.component.scss']
})
export class AlertMessageComponent implements OnInit {
public swapStateEnum = SwapStateEnum;
public showQRField = '';
public showQRName = '';
public showCopyName = '';

@ -153,6 +153,12 @@ export class SideNavigationComponent implements OnInit, OnDestroy {
let clonedMenu = [];
clonedMenu = JSON.parse(JSON.stringify(MENU_DATA.LNDChildren));
this.navMenus.data = clonedMenu.filter(navMenuData => {
if(navMenuData.children && navMenuData.children.length) {
navMenuData.children = navMenuData.children.filter(navMenuChild => {
return ((navMenuChild.userPersona === UserPersonaEnum.ALL || navMenuChild.userPersona === this.settings.userPersona) && navMenuChild.link !== '/lnd/loop')
|| (navMenuChild.link === '/lnd/loop' && this.settings.swapServerUrl && this.settings.swapServerUrl.trim() !== '');
});
}
return navMenuData.userPersona === UserPersonaEnum.ALL || navMenuData.userPersona === this.settings.userPersona;
});
}

@ -67,8 +67,8 @@ export class AppSettingsComponent implements OnInit, OnDestroy {
onCurrencyChange(event: any) {
this.selNode.settings.currencyUnits = [...CURRENCY_UNITS, event.value];
this.store.dispatch(new RTLActions.SetChildNodeSettings({userPersona: this.selNode.settings.userPersona, channelBackupPath: this.selNode.settings.channelBackupPath, selCurrencyUnit: event.value, currencyUnits: this.selNode.settings.currencyUnits, fiatConversion: this.selNode.settings.fiatConversion, lnImplementation: this.selNode.lnImplementation}));
this.store.dispatch(new RTLActions.SetChildNodeSettingsCL({userPersona: this.selNode.settings.userPersona, channelBackupPath: this.selNode.settings.channelBackupPath, selCurrencyUnit: event.value, currencyUnits: this.selNode.settings.currencyUnits, fiatConversion: this.selNode.settings.fiatConversion, lnImplementation: this.selNode.lnImplementation}));
this.store.dispatch(new RTLActions.SetChildNodeSettings({userPersona: this.selNode.settings.userPersona, channelBackupPath: this.selNode.settings.channelBackupPath, selCurrencyUnit: event.value, currencyUnits: this.selNode.settings.currencyUnits, fiatConversion: this.selNode.settings.fiatConversion, lnImplementation: this.selNode.lnImplementation, swapServerUrl: this.selNode.settings.swapServerUrl}));
this.store.dispatch(new RTLActions.SetChildNodeSettingsCL({userPersona: this.selNode.settings.userPersona, channelBackupPath: this.selNode.settings.channelBackupPath, selCurrencyUnit: event.value, currencyUnits: this.selNode.settings.currencyUnits, fiatConversion: this.selNode.settings.fiatConversion, lnImplementation: this.selNode.lnImplementation, swapServerUrl: this.selNode.settings.swapServerUrl}));
}
toggleSettings(toggleField: string, event?: any) {
@ -90,8 +90,8 @@ export class AppSettingsComponent implements OnInit, OnDestroy {
this.logger.info(this.selNode.settings);
this.store.dispatch(new RTLActions.OpenSpinner('Updating Settings...'));
this.store.dispatch(new RTLActions.SaveSettings({settings: this.selNode.settings, defaultNodeIndex: defaultNodeIndex}));
this.store.dispatch(new RTLActions.SetChildNodeSettings({userPersona: this.selNode.settings.userPersona, channelBackupPath: this.selNode.settings.channelBackupPath, selCurrencyUnit: this.selNode.settings.currencyUnit, currencyUnits: this.selNode.settings.currencyUnits, fiatConversion: this.selNode.settings.fiatConversion, lnImplementation: this.selNode.lnImplementation}));
this.store.dispatch(new RTLActions.SetChildNodeSettingsCL({userPersona: this.selNode.settings.userPersona, channelBackupPath: this.selNode.settings.channelBackupPath, selCurrencyUnit: this.selNode.settings.currencyUnit, currencyUnits: this.selNode.settings.currencyUnits, fiatConversion: this.selNode.settings.fiatConversion, lnImplementation: this.selNode.lnImplementation}));
this.store.dispatch(new RTLActions.SetChildNodeSettings({userPersona: this.selNode.settings.userPersona, channelBackupPath: this.selNode.settings.channelBackupPath, selCurrencyUnit: this.selNode.settings.currencyUnit, currencyUnits: this.selNode.settings.currencyUnits, fiatConversion: this.selNode.settings.fiatConversion, lnImplementation: this.selNode.lnImplementation, swapServerUrl: this.selNode.settings.swapServerUrl}));
this.store.dispatch(new RTLActions.SetChildNodeSettingsCL({userPersona: this.selNode.settings.userPersona, channelBackupPath: this.selNode.settings.channelBackupPath, selCurrencyUnit: this.selNode.settings.currencyUnit, currencyUnits: this.selNode.settings.currencyUnits, fiatConversion: this.selNode.settings.fiatConversion, lnImplementation: this.selNode.lnImplementation, swapServerUrl: this.selNode.settings.swapServerUrl}));
this.done.emit();
}

@ -17,6 +17,7 @@ export class Settings {
public bitcoindConfigPath?: string,
public enableLogging?: boolean,
public lnServerUrl?: string,
public swapServerUrl?: string,
public channelBackupPath?: string,
public currencyUnit?: string
) { }
@ -66,6 +67,7 @@ export interface SelNodeChild {
currencyUnits?: string[];
fiatConversion?: boolean;
lnImplementation?: string;
swapServerUrl?: string;
}
export class HelpTopic {

@ -1,7 +1,8 @@
import { DataTypeEnum } from '../services/consts-enums-functions';
import { DataTypeEnum, SwapTypeEnum } from '../services/consts-enums-functions';
import { GetInfoRoot } from './RTLconfig';
import { GetInfo, Invoice, Channel } from './lndModels';
import { InvoiceCL, GetInfoCL } from './clModels';
import { LoopQuote } from './loopModels';
export interface MessageErrorField {
code: number;
@ -72,6 +73,14 @@ export interface ShowPubkeyData {
component?: any;
}
export interface LoopAlert {
channel: Channel;
minQuote: LoopQuote;
maxQuote: LoopQuote;
direction?: SwapTypeEnum;
component?: any;
}
export interface AlertData {
type: string; // INFORMATION/WARNING/SUCCESS/ERROR
alertTitle?: string;
@ -108,5 +117,5 @@ export interface ErrorData {
export interface DialogConfig {
width?: string;
minHeight?: string;
data: AlertData | ConfirmationData | ErrorData | OpenChannelAlert | CLOpenChannelAlert | InvoiceInformation | CLInvoiceInformation | ChannelInformation | OnChainAddressInformation | ShowPubkeyData;
data: AlertData | ConfirmationData | ErrorData | OpenChannelAlert | CLOpenChannelAlert | InvoiceInformation | CLInvoiceInformation | ChannelInformation | OnChainAddressInformation | ShowPubkeyData | LoopAlert;
}

@ -1,3 +1,5 @@
import { SwapStateEnum, SwapTypeEnum } from '../services/consts-enums-functions';
export interface ChannelStatus {
channels?: number;
capacity?:number;
@ -417,3 +419,20 @@ export interface PendingChannelsData {
num_channels: number;
limbo_balance: number;
}
export interface SwapStatus {
type?: SwapTypeEnum;
cost_server?: string;
cost_offchain?: string;
htlc_address?: string;
state?: SwapStateEnum;
amt?: string;
cost_onchain?: string;
initiation_time?: string;
initiation_time_str?: string;
id_bytes?: string;
last_update_time?: string;
last_update_time_str?: string;
id?: string;
}

@ -0,0 +1,21 @@
export interface LoopTerms {
min_swap_amount?: string;
max_swap_amount?: string;
}
export interface LoopQuote {
amount?: number;
swap_fee?: string;
miner_fee?: string;
prepay_amt?: string;
cltv_delta?: number;
swap_payment_dest?: string;
off_chain_swap_routing_fee_percentage?: number;
}
export interface LoopStatus {
htlc_address?: string;
id_bytes?: string;
id?: string;
error?: any;
}

@ -1,4 +1,4 @@
import { faTachometerAlt, faLink, faBolt, faExchangeAlt, faUsers, faMapSigns, faQuestion, faSearch, faTools, faProjectDiagram, faDownload, faServer, faPercentage, faUserCheck } from '@fortawesome/free-solid-svg-icons';
import { faTachometerAlt, faLink, faBolt, faExchangeAlt, faUsers, faMapSigns, faQuestion, faSearch, faTools, faProjectDiagram, faDownload, faServer, faPercentage, faInfinity, faUserCheck } from '@fortawesome/free-solid-svg-icons';
import { UserPersonaEnum } from '../services/consts-enums-functions';
export const MENU_DATA: MenuRootNode = {
@ -11,6 +11,7 @@ export const MENU_DATA: MenuRootNode = {
{id: 33, parentId: 3, name: 'Routing', iconType: 'FA', icon: faMapSigns, link: '/lnd/routing', userPersona: UserPersonaEnum.ALL},
{id: 34, parentId: 3, name: 'Graph Lookup', iconType: 'FA', icon: faSearch, link: '/lnd/lookups', userPersona: UserPersonaEnum.ALL},
{id: 35, parentId: 3, name: 'Sign/Verify', iconType: 'FA', icon: faUserCheck, link: '/lnd/signverify', userPersona: UserPersonaEnum.ALL},
{id: 36, parentId: 3, name: 'Loop', iconType: 'FA', icon: faInfinity, link: '/lnd/loop', userPersona: UserPersonaEnum.ALL},
{id: 37, parentId: 3, name: 'Backup', iconType: 'FA', icon: faDownload, link: '/lnd/backup', userPersona: UserPersonaEnum.ALL}
]},
{id: 5, parentId: 0, name: 'Network', iconType: 'FA', icon: faProjectDiagram, link: '/lnd/network', userPersona: UserPersonaEnum.OPERATOR},

@ -118,3 +118,17 @@ export const CHANNEL_CLOSURE_TYPE = {
FUNDING_CANCELED: { name: 'Funding Canceled', tooltip: 'Channel never fully opened' },
ABANDONED: { name: 'Abandoned', tooltip: 'Channel abandoned by the local node' }
}
export enum SwapStateEnum {
INITIATED = 'Initiated',
PREIMAGE_REVEALED = 'Preimage Revealed',
HTLC_PUBLISHED = 'HTLC Published',
SUCCESS = 'Successful',
FAILED = 'Failed',
INVOICE_SETTLED = 'Invoice Settled'
}
export enum SwapTypeEnum {
LOOP_OUT = 'LOOP_OUT',
LOOP_IN = 'LOOP_IN'
}

@ -0,0 +1,138 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { throwError, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { environment, API_URL } from '../../../environments/environment';
import { ErrorMessageComponent } from '../../shared/components/data-modal/error-message/error-message.component';
import { LoggerService } from '../../shared/services/logger.service';
import { AlertTypeEnum } from '../../shared/services/consts-enums-functions';
import * as RTLActions from '../../store/rtl.actions';
import * as fromRTLReducer from '../../store/rtl.reducers';
@Injectable()
export class LoopService {
private CHILD_API_URL = API_URL + '/lnd';
private loopUrl = '';
constructor(private httpClient: HttpClient, private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>) {}
loopOut(amount: number, chanId: string, targetConf: number, swapRoutingFee: number, minerFee: number, prepayRoutingFee: number, prepayAmt: number, swapFee: number, swapPublicationDeadline: number, destAddress: string) {
let requestBody = { amount: amount, targetConf: targetConf, swapRoutingFee: swapRoutingFee, minerFee: minerFee, prepayRoutingFee: prepayRoutingFee, prepayAmt: prepayAmt, swapFee: swapFee, swapPublicationDeadline: swapPublicationDeadline, destAddress: destAddress };
if (chanId !== '') { requestBody['chanId'] = chanId; }
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/out';
return this.httpClient.post(this.loopUrl, requestBody).pipe(catchError(err => this.handleErrorWithoutAlert('Loop Out for Channel: ' + chanId, err)));
}
getLoopOutTerms() {
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/out/terms';
return this.httpClient.get(this.loopUrl).pipe(catchError(err => this.handleErrorWithoutAlert('Loop Out Terms', err)));
}
getLoopOutQuote(amount: number, targetConf: number, swapPublicationDeadline: number) {
let params = new HttpParams();
params = params.append('targetConf', targetConf.toString());
params = params.append('swapPublicationDeadline', swapPublicationDeadline.toString());
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/out/quote/' + amount;
return this.httpClient.get(this.loopUrl, { params: params }).pipe(catchError(err => this.handleErrorWithoutAlert('Loop Out Quote', err)));
}
getLoopOutTermsAndQuotes(targetConf: number) {
let params = new HttpParams();
params = params.append('targetConf', targetConf.toString());
params = params.append('swapPublicationDeadline', (new Date().getTime() + (30 * 60000)).toString());
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/out/termsAndQuotes';
return this.httpClient.get(this.loopUrl, { params: params }).pipe(catchError(err => {
return this.handleErrorWithAlert('Loop Out Terms and Quotes', err);
}));
}
loopIn(amount: number, swapFee: number, minerFee: number, lastHop: string, externalHtlc: boolean) {
const requestBody = { amount: amount, swapFee: swapFee, minerFee: minerFee, lastHop: lastHop, externalHtlc: externalHtlc };
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/in';
return this.httpClient.post(this.loopUrl, requestBody).pipe(catchError(err => this.handleErrorWithoutAlert('Loop In', err)));
}
getLoopInTerms() {
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/in/terms';
return this.httpClient.get(this.loopUrl).pipe(catchError(err => this.handleErrorWithoutAlert('Loop In Terms', err)));
}
getLoopInQuote(amount: number, targetConf: string, swapPublicationDeadline: number) {
let params = new HttpParams();
params = params.append('targetConf', targetConf.toString());
params = params.append('swapPublicationDeadline', swapPublicationDeadline.toString());
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/in/quote/' + amount;
return this.httpClient.get(this.loopUrl, { params: params }).pipe(catchError(err => this.handleErrorWithoutAlert('Loop In Qoute', err)));
}
getLoopInTermsAndQuotes(targetConf: number) {
let params = new HttpParams();
params = params.append('targetConf', targetConf.toString());
params = params.append('swapPublicationDeadline', (new Date().getTime() + (30 * 60000)).toString());
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/in/termsAndQuotes';
return this.httpClient.get(this.loopUrl, { params: params }).pipe(catchError(err => {
return this.handleErrorWithAlert('Loop In Terms and Quotes', err);
}));
}
getSwap(id: string) {
this.loopUrl = this.CHILD_API_URL + environment.LOOP_API + '/swap/' + id;
return this.httpClient.get(this.loopUrl).pipe(catchError(err => this.handleErrorWithoutAlert('Loop Get Swap for ID: ' + id, err)));
}
handleErrorWithoutAlert(actionName: string, err: { status: number, error: any }) {
this.logger.error('ERROR IN: ' + actionName + '\n' + JSON.stringify(err));
this.store.dispatch(new RTLActions.CloseSpinner())
if (err.status === 401) {
this.logger.info('Redirecting to Login');
this.store.dispatch(new RTLActions.Logout());
} else if (err.error.errno === 'ECONNREFUSED' || err.error.error.errno === 'ECONNREFUSED') {
this.store.dispatch(new RTLActions.OpenAlert({
data: {
type: 'ERROR',
alertTitle: 'Loop Not Connected',
message: { code: 'ECONNREFUSED', message: 'Unable to Connect to Loop Server', URL: actionName },
component: ErrorMessageComponent
}
}));
}
return throwError(err);
}
handleErrorWithAlert(errURL: string, err: any) {
if (typeof err.error.error === 'string') {
try {
err = JSON.parse(err.error.error);
} catch(err) {}
} else {
err = err.error.error.error ? err.error.error.error : err.error.error ? err.error.error : err.error ? err.error : { code : 500, message: 'Unknown Error' };
}
this.logger.error(err);
this.store.dispatch(new RTLActions.CloseSpinner())
if (err.status === 401) {
this.logger.info('Redirecting to Login');
this.store.dispatch(new RTLActions.Logout());
} else if (err.errno === 'ECONNREFUSED') {
this.store.dispatch(new RTLActions.OpenAlert({
data: {
type: 'ERROR',
alertTitle: 'Loop Not Connected',
message: { code: 'ECONNREFUSED', message: 'Unable to Connect to Loop Server', URL: errURL },
component: ErrorMessageComponent
}
}));
} else {
this.store.dispatch(new RTLActions.OpenAlert({data: {
type: AlertTypeEnum.ERROR,
alertTitle: 'ERROR',
message: { code: err.code ? err.code : err.status, message: err.message ? err.message : 'Unknown Error', URL: errURL },
component: ErrorMessageComponent
}
}));
}
return throwError(err);
}
}

@ -35,6 +35,7 @@ import { ServerConfigComponent } from './components/settings/server-config/serve
import { ErrorComponent } from './components/error/error.component';
import { CurrencyUnitConverterComponent } from './components/currency-unit-converter/currency-unit-converter.component';
import { AuthSettingsComponent } from './components/settings/auth-settings/auth-settings.component';
import { LoopQuoteComponent } from '../lnd/loop/loop-quote/loop-quote.component';
import { ClipboardDirective } from './directive/clipboard.directive';
import { AutoFocusDirective } from './directive/auto-focus.directive';
import { MaxValidator } from './directive/max-amount.directive';
@ -42,6 +43,9 @@ import { MinValidator } from './directive/min-amount.directive';
import { RemoveLeadingZerosPipe } from './pipes/app.pipe';
import { LoggerService, ConsoleLoggerService } from '../shared/services/logger.service';
import { LoopStatusComponent } from '../lnd/loop/loop-status/loop-status.component';
import { LoopOutInfoGraphicsComponent } from '../lnd/loop/loop-out-info-graphics/info-graphics.component';
import { LoopInInfoGraphicsComponent } from '../lnd/loop/loop-in-info-graphics/info-graphics.component';
@NgModule({
imports: [
@ -135,7 +139,11 @@ import { LoggerService, ConsoleLoggerService } from '../shared/services/logger.s
MinValidator,
QRCodeModule,
RemoveLeadingZerosPipe,
PerfectScrollbarModule
PerfectScrollbarModule,
LoopQuoteComponent,
LoopStatusComponent,
LoopOutInfoGraphicsComponent,
LoopInInfoGraphicsComponent
],
declarations: [
AppSettingsComponent,
@ -153,7 +161,11 @@ import { LoggerService, ConsoleLoggerService } from '../shared/services/logger.s
MaxValidator,
MinValidator,
RemoveLeadingZerosPipe,
AuthSettingsComponent
AuthSettingsComponent,
LoopQuoteComponent,
LoopStatusComponent,
LoopOutInfoGraphicsComponent,
LoopInInfoGraphicsComponent
],
providers: [
{ provide: LoggerService, useClass: ConsoleLoggerService },

@ -80,6 +80,7 @@ body {
.mat-sidenav-container .mat-sidenav-content {
height: 100vh;
min-height: 100vh;
}
.sidenav {

@ -388,4 +388,7 @@
fill: $primary-lighter;
}
.mat-expansion-panel-header[aria-disabled='true'] {
color: $foreground-text;
}
}

@ -6,7 +6,7 @@ import { RTLConfiguration, Settings, ConfigSettingsNode, GetInfoRoot, SelNodeChi
import { GetInfoCL, FeesCL, PeerCL, PaymentCL, PayRequestCL, QueryRoutesCL, ChannelCL, FeeRatesCL, ForwardingHistoryResCL, InvoiceCL, ListInvoicesCL, OnChainCL } from '../shared/models/clModels';
import {
GetInfo, Peer, Balance, NetworkInfo, Fees, Channel, Invoice, ListInvoices, Payment, GraphNode,
PayRequest, ChannelsTransaction, PendingChannels, ClosedChannel, Transaction, SwitchReq, SwitchRes, QueryRoutes, PendingChannelsGroup
PayRequest, ChannelsTransaction, PendingChannels, ClosedChannel, Transaction, SwitchReq, SwitchRes, QueryRoutes, PendingChannelsGroup, SwapStatus
} from '../shared/models/lndModels';
export const VOID = 'VOID';
@ -105,6 +105,8 @@ export const GET_FORWARDING_HISTORY = 'GET_FORWARDING_HISTORY';
export const SET_FORWARDING_HISTORY = 'SET_FORWARDING_HISTORY';
export const GET_QUERY_ROUTES = 'GET_QUERY_ROUTES';
export const SET_QUERY_ROUTES = 'SET_QUERY_ROUTES';
export const FETCH_LOOP_SWAPS = 'FETCH_LOOP_SWAPS';
export const SET_LOOP_SWAPS = 'SET_LOOP_SWAPS';
export const RESET_CL_STORE = 'RESET_CL_STORE';
export const CLEAR_EFFECT_ERROR_CL = 'CLEAR_EFFECT_ERROR_CL';
@ -596,6 +598,16 @@ export class SetQueryRoutes implements Action {
constructor(public payload: QueryRoutes) {}
}
export class FetchLoopSwaps implements Action {
readonly type = FETCH_LOOP_SWAPS;
constructor() {}
}
export class SetLoopSwaps implements Action {
readonly type = SET_LOOP_SWAPS;
constructor(public payload: SwapStatus[]) {}
}
export class IsAuthorized implements Action {
readonly type = IS_AUTHORIZED;
constructor(public payload: string) {} // payload = password
@ -863,7 +875,7 @@ export type RTLActions =
GetNewAddress | SetNewAddress | SetChannelTransaction |
GenSeed | GenSeedResponse | InitWallet | InitWalletResponse | UnlockWallet |
FetchConfig | ShowConfig | PeerLookup | ChannelLookup | InvoiceLookup | SetLookup |
IsAuthorized | IsAuthorizedRes | Login | Logout | ResetPassword |
FetchLoopSwaps | SetLoopSwaps | IsAuthorized | IsAuthorizedRes | Login | Logout | ResetPassword |
SetChildNodeSettingsCL | FetchInfoCL | SetInfoCL | FetchFeesCL | SetFeesCL | FetchFeeRatesCL | SetFeeRatesCL |
FetchBalanceCL | SetBalanceCL | FetchLocalRemoteBalanceCL | SetLocalRemoteBalanceCL |
GetNewAddressCL | SetNewAddressCL |

@ -378,9 +378,9 @@ export class RTLEffects implements OnDestroy {
const landingPage = isInitialSetup ? '' : 'HOME';
let selNode = {};
if(node.settings.fiatConversion && node.settings.currencyUnit) {
selNode = { userPersona: node.settings.userPersona, channelBackupPath: node.settings.channelBackupPath, selCurrencyUnit: node.settings.currencyUnit, currencyUnits: [...CURRENCY_UNITS, node.settings.currencyUnit], fiatConversion: node.settings.fiatConversion, lnImplementation: node.lnImplementation };
selNode = { userPersona: node.settings.userPersona, channelBackupPath: node.settings.channelBackupPath, selCurrencyUnit: node.settings.currencyUnit, currencyUnits: [...CURRENCY_UNITS, node.settings.currencyUnit], fiatConversion: node.settings.fiatConversion, lnImplementation: node.lnImplementation, swapServerUrl: node.settings.swapServerUrl };
} else {
selNode = { userPersona: node.settings.userPersona, channelBackupPath: node.settings.channelBackupPath, selCurrencyUnit: node.settings.currencyUnit, currencyUnits: CURRENCY_UNITS, fiatConversion: node.settings.fiatConversion, lnImplementation: node.lnImplementation };
selNode = { userPersona: node.settings.userPersona, channelBackupPath: node.settings.channelBackupPath, selCurrencyUnit: node.settings.currencyUnit, currencyUnits: CURRENCY_UNITS, fiatConversion: node.settings.fiatConversion, lnImplementation: node.lnImplementation, swapServerUrl: node.settings.swapServerUrl };
}
this.store.dispatch(new RTLActions.ResetRootStore(node));
this.store.dispatch(new RTLActions.ResetLNDStore(selNode));

@ -22,6 +22,7 @@ export const environment = {
INVOICES_API: '/invoices',
SWITCH_API: '/switch',
ON_CHAIN_API: '/onchain',
LOOP_API: '/loop',
MESSAGE_API: '/message',
VERSION: VERSION
};

@ -22,6 +22,7 @@ export const environment = {
INVOICES_API: '/invoices',
SWITCH_API: '/switch',
ON_CHAIN_API: '/onchain',
LOOP_API: '/loop',
MESSAGE_API: '/message',
VERSION: VERSION
};

@ -1 +1 @@
export const VERSION = '0.6.8-beta';
export const VERSION = '0.7.0-beta';
Loading…
Cancel
Save