parent
ee797481b4
commit
86d7d1ffa8
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -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;
|
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: </strong>{{channel.remote_alias | titlecase}}</p>
|
||||||
|
<p fxFlex="30"><strong>Channel ID: </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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
export const VERSION = '0.6.8-beta';
|
export const VERSION = '0.7.0-beta';
|
Loading…
Reference in New Issue